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: ...@@ -14,7 +14,7 @@ linters:
# Whether or not to prefer `border: 0` over `border: none`. # Whether or not to prefer `border: 0` over `border: none`.
BorderZero: BorderZero:
enabled: false enabled: true
# Reports when you define a rule set using a selector with chained classes # Reports when you define a rule set using a selector with chained classes
# (a.k.a. adjoining classes). # (a.k.a. adjoining classes).
......
...@@ -68,7 +68,6 @@ $(() => { ...@@ -68,7 +68,6 @@ $(() => {
rootPath: $boardApp.dataset.rootPath, rootPath: $boardApp.dataset.rootPath,
bulkUpdatePath: $boardApp.dataset.bulkUpdatePath, bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
detailIssue: Store.detail, detailIssue: Store.detail,
milestoneTitle: $boardApp.dataset.boardMilestoneTitle,
defaultAvatar: $boardApp.dataset.defaultAvatar, defaultAvatar: $boardApp.dataset.defaultAvatar,
}, },
computed: { computed: {
...@@ -77,16 +76,6 @@ $(() => { ...@@ -77,16 +76,6 @@ $(() => {
}, },
}, },
created () { 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({ gl.boardService = new BoardService({
boardsEndpoint: this.boardsEndpoint, boardsEndpoint: this.boardsEndpoint,
listsEndpoint: this.listsEndpoint, listsEndpoint: this.listsEndpoint,
...@@ -102,7 +91,7 @@ $(() => { ...@@ -102,7 +91,7 @@ $(() => {
eventHub.$off('updateTokens', this.updateTokens); eventHub.$off('updateTokens', this.updateTokens);
}, },
mounted () { mounted () {
this.filterManager = new FilteredSearchBoards(Store.filter, true, [(this.milestoneTitle ? 'milestone' : null)]); this.filterManager = new FilteredSearchBoards(Store.filter, true, Store.cantEdit);
this.filterManager.setup(); this.filterManager.setup();
Store.disabled = this.disabled; Store.disabled = this.disabled;
...@@ -146,6 +135,40 @@ $(() => { ...@@ -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({ gl.IssueBoardsModalAddBtn = new Vue({
mixins: [gl.issueBoards.ModalMixins], mixins: [gl.issueBoards.ModalMixins],
el: document.getElementById('js-add-issues-btn'), 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 { ...@@ -51,10 +51,6 @@ export default {
project_id: this.selectedProject.id, 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}`); eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel(); this.cancel();
......
import Vue from 'vue'; import Vue from 'vue';
import './board_new_form'; import BoardForm from './board_form.vue';
(() => { (() => {
window.gl = window.gl || {}; window.gl = window.gl || {};
...@@ -11,7 +11,7 @@ import './board_new_form'; ...@@ -11,7 +11,7 @@ import './board_new_form';
gl.issueBoards.BoardsSelector = Vue.extend({ gl.issueBoards.BoardsSelector = Vue.extend({
components: { components: {
'board-selector-form': gl.issueBoards.BoardSelectorForm, BoardForm,
}, },
props: { props: {
currentBoard: { currentBoard: {
...@@ -60,19 +60,6 @@ import './board_new_form'; ...@@ -60,19 +60,6 @@ import './board_new_form';
showDelete() { showDelete() {
return this.boards.length > 1; 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: { methods: {
showPage(page) { showPage(page) {
......
...@@ -158,7 +158,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({ ...@@ -158,7 +158,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({
class="card-number" class="card-number"
v-if="issueId" 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> </span>
</h4> </h4>
<div class="card-assignee"> <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({ ...@@ -30,11 +30,16 @@ gl.issueBoards.ModalFooter = Vue.extend({
const list = this.modal.selectedList || this.state.lists[firstListIndex]; const list = this.modal.selectedList || this.state.lists[firstListIndex];
const selectedIssues = ModalStore.getSelectedIssues(); const selectedIssues = ModalStore.getSelectedIssues();
const issueIds = selectedIssues.map(issue => issue.id); 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 // Post the data to the backend
gl.boardService.bulkUpdate(issueIds, { gl.boardService.bulkUpdate(issueIds, {
add_label_ids: [list.label.id], add_label_ids: [list.label.id, ...boardLabelIds],
milestone_id: this.state.currentBoard.milestone_id, milestone_id: currentBoard.milestone_id,
assignee_ids: assigneeIds,
weight: currentBoard.weight,
}).catch(() => { }).catch(() => {
new Flash('Failed to update issues, please try again.', 'alert'); new Flash('Failed to update issues, please try again.', 'alert');
......
...@@ -30,24 +30,43 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({ ...@@ -30,24 +30,43 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
}, },
methods: { methods: {
removeIssue() { removeIssue() {
const board = Store.state.currentBoard;
const issue = this.issue; const issue = this.issue;
const lists = issue.getLists(); const lists = issue.getLists();
const boardLabelIds = board.labels.map(label => label.id);
const listLabelIds = lists.map(list => list.label.id); const listLabelIds = lists.map(list => list.label.id);
let labelIds = this.issue.labels
let labelIds = issue.labels
.map(label => label.id) .map(label => label.id)
.filter(id => !listLabelIds.includes(id)); .filter(id => !listLabelIds.includes(id))
.filter(id => !boardLabelIds.includes(id));
if (labelIds.length === 0) { if (labelIds.length === 0) {
labelIds = ['']; 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 = { const data = {
issue: { issue: {
label_ids: labelIds, label_ids: labelIds,
assignee_ids: assigneeIds,
}, },
}; };
if (Store.state.currentBoard.milestone_id) {
if (board.milestone_id) {
data.issue.milestone_id = -1; data.issue.milestone_id = -1;
} }
if (board.weight) {
data.issue.weight = null;
}
// Post the remove data // Post the remove data
Vue.http.patch(this.updateUrl, data).catch(() => { Vue.http.patch(this.updateUrl, data).catch(() => {
new Flash('Failed to remove issue from board, please try again.', 'alert'); 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 { ...@@ -11,7 +11,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Issue boards is slightly different, we handle all the requests async // Issue boards is slightly different, we handle all the requests async
// instead or reloading the page, we just re-fire the list ajax requests // instead or reloading the page, we just re-fire the list ajax requests
this.isHandledAsync = true; this.isHandledAsync = true;
this.cantEdit = cantEdit; this.cantEdit = cantEdit.filter(i => typeof i === 'string');
this.cantEditWithValue = cantEdit.filter(i => typeof i === 'object');
} }
updateObject(path) { updateObject(path) {
...@@ -42,7 +43,9 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { ...@@ -42,7 +43,9 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
this.filteredSearchInput.dispatchEvent(new Event('input')); this.filteredSearchInput.dispatchEvent(new Event('input'));
} }
canEdit(tokenName) { canEdit(tokenName, tokenValue) {
return this.cantEdit.indexOf(tokenName) === -1; 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 { ...@@ -20,6 +20,7 @@ class ListIssue {
this.position = obj.relative_position || Infinity; this.position = obj.relative_position || Infinity;
this.milestone_id = obj.milestone_id; this.milestone_id = obj.milestone_id;
this.project_id = obj.project_id; this.project_id = obj.project_id;
this.weight = obj.weight;
if (obj.project) { if (obj.project) {
this.project = new IssueProject(obj.project); this.project = new IssueProject(obj.project);
......
...@@ -114,6 +114,8 @@ class List { ...@@ -114,6 +114,8 @@ class List {
issue.iid = data.iid; issue.iid = data.iid;
issue.milestone = data.milestone; issue.milestone = data.milestone;
issue.project = data.project; issue.project = data.project;
issue.assignees = data.assignees;
issue.labels = data.labels;
if (this.issuesSize > 1) { if (this.issuesSize > 1) {
const moveBeforeId = this.issues[1].id; const moveBeforeId = this.issues[1].id;
......
...@@ -30,10 +30,28 @@ class BoardService { ...@@ -30,10 +30,28 @@ class BoardService {
} }
createBoard (board) { 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) { 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 () { all () {
...@@ -99,17 +117,6 @@ class BoardService { ...@@ -99,17 +117,6 @@ class BoardService {
return this.issues.bulkUpdate(data); 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; window.BoardService = BoardService;
...@@ -13,9 +13,15 @@ gl.issueBoards.BoardsStore = { ...@@ -13,9 +13,15 @@ gl.issueBoards.BoardsStore = {
filter: { filter: {
path: '', path: '',
}, },
state: {}, state: {
currentBoard: {
labels: [],
},
currentPage: '',
reload: false,
},
detail: { detail: {
issue: {} issue: {},
}, },
moving: { moving: {
issue: {}, issue: {},
...@@ -24,13 +30,21 @@ gl.issueBoards.BoardsStore = { ...@@ -24,13 +30,21 @@ gl.issueBoards.BoardsStore = {
create () { create () {
this.state.lists = []; this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&'); this.filter.path = getUrlParamsArray().join('&');
this.detail = { issue: {} }; this.detail = {
issue: {},
};
}, },
createNewListDropdownData() { createNewListDropdownData() {
this.state.currentBoard = {}; this.state.currentBoard = {
labels: [],
};
this.state.currentPage = ''; this.state.currentPage = '';
this.state.reload = false; this.state.reload = false;
}, },
showPage(page) {
this.state.reload = false;
this.state.currentPage = page;
},
addList (listObj, defaultAvatar) { addList (listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar); const list = new List(listObj, defaultAvatar);
this.state.lists.push(list); this.state.lists.push(list);
......
...@@ -147,6 +147,16 @@ class DropdownUtils { ...@@ -147,6 +147,16 @@ class DropdownUtils {
return dataValue !== null; 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) // Determines the full search query (visual tokens + input)
static getSearchQuery(untilInput = false) { static getSearchQuery(untilInput = false) {
const container = FilteredSearchContainer.container; const container = FilteredSearchContainer.container;
......
...@@ -198,8 +198,8 @@ class FilteredSearchManager { ...@@ -198,8 +198,8 @@ class FilteredSearchManager {
if (e.keyCode === 8 || e.keyCode === 46) { if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const sanitizedTokenName = lastVisualToken && lastVisualToken.querySelector('.name').textContent.trim(); const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken);
const canEdit = sanitizedTokenName && this.canEdit && this.canEdit(sanitizedTokenName); const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) { if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial(); gl.FilteredSearchVisualTokens.removeLastTokenPartial();
...@@ -349,8 +349,8 @@ class FilteredSearchManager { ...@@ -349,8 +349,8 @@ class FilteredSearchManager {
let canClearToken = t.classList.contains('js-visual-token'); let canClearToken = t.classList.contains('js-visual-token');
if (canClearToken) { if (canClearToken) {
const tokenKey = t.querySelector('.name').textContent.trim(); const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(t);
canClearToken = this.canEdit && this.canEdit(tokenKey); canClearToken = this.canEdit && this.canEdit(tokenName, tokenValue);
} }
if (canClearToken) { if (canClearToken) {
...@@ -482,7 +482,7 @@ class FilteredSearchManager { ...@@ -482,7 +482,7 @@ class FilteredSearchManager {
} }
hasFilteredSearch = true; hasFilteredSearch = true;
const canEdit = this.canEdit && this.canEdit(sanitizedKey); const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue);
gl.FilteredSearchVisualTokens.addFilterVisualToken( gl.FilteredSearchVisualTokens.addFilterVisualToken(
sanitizedKey, sanitizedKey,
`${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`,
......
...@@ -38,21 +38,14 @@ class FilteredSearchVisualTokens { ...@@ -38,21 +38,14 @@ class FilteredSearchVisualTokens {
} }
static createVisualTokenElementHTML(canEdit = true) { static createVisualTokenElementHTML(canEdit = true) {
let removeTokenMarkup = '';
if (canEdit) {
removeTokenMarkup = `
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
`;
}
return ` return `
<div class="selectable" role="button"> <div class="${canEdit ? 'selectable' : 'hidden'}" role="button">
<div class="name"></div> <div class="name"></div>
<div class="value-container"> <div class="value-container">
<div class="value"></div> <div class="value"></div>
${removeTokenMarkup} <div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
</div> </div>
</div> </div>
`; `;
......
...@@ -7,7 +7,7 @@ import DropdownUtils from './filtered_search/dropdown_utils'; ...@@ -7,7 +7,7 @@ import DropdownUtils from './filtered_search/dropdown_utils';
import CreateLabelDropdown from './create_label'; import CreateLabelDropdown from './create_label';
export default class LabelsSelect { export default class LabelsSelect {
constructor(els) { constructor(els, options = {}) {
var _this, $els; var _this, $els;
_this = this; _this = this;
...@@ -57,6 +57,7 @@ export default class LabelsSelect { ...@@ -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> <% }); %>'); 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>'; labelNoneHTMLTemplate = '<span class="no-value">None</span>';
} }
const handleClick = options.handleClick;
$sidebarLabelTooltip.tooltip(); $sidebarLabelTooltip.tooltip();
...@@ -315,9 +316,9 @@ export default class LabelsSelect { ...@@ -315,9 +316,9 @@ export default class LabelsSelect {
}, },
multiSelect: $dropdown.hasClass('js-multiselect'), multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(options) { clicked: function(clickEvent) {
const { $el, e, isMarking } = options; const { $el, e, isMarking } = clickEvent;
const label = options.selectedObj; const label = clickEvent.selectedObj;
var isIssueIndex, isMRIndex, page, boardsModel; var isIssueIndex, isMRIndex, page, boardsModel;
var fadeOutLoader = () => { var fadeOutLoader = () => {
...@@ -390,6 +391,10 @@ export default class LabelsSelect { ...@@ -390,6 +391,10 @@ export default class LabelsSelect {
.then(fadeOutLoader) .then(fadeOutLoader)
.catch(fadeOutLoader); .catch(fadeOutLoader);
} }
else if (handleClick) {
e.preventDefault();
handleClick(label);
}
else { else {
if ($dropdown.hasClass('js-multiselect')) { if ($dropdown.hasClass('js-multiselect')) {
......
...@@ -5,7 +5,7 @@ import _ from 'underscore'; ...@@ -5,7 +5,7 @@ import _ from 'underscore';
(function() { (function() {
this.MilestoneSelect = (function() { this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject, els) { function MilestoneSelect(currentProject, els, options = {}) {
var _this, $els; var _this, $els;
if (currentProject != null) { if (currentProject != null) {
_this = this; _this = this;
...@@ -141,7 +141,7 @@ import _ from 'underscore'; ...@@ -141,7 +141,7 @@ import _ from 'underscore';
}, },
opened: function(e) { opened: function(e) {
const $el = $(e.currentTarget); 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; selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
} }
$('a.is-active', $el).removeClass('is-active'); $('a.is-active', $el).removeClass('is-active');
...@@ -150,7 +150,8 @@ import _ from 'underscore'; ...@@ -150,7 +150,8 @@ import _ from 'underscore';
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
hideRow: function(milestone) { hideRow: function(milestone) {
if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') && 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; return milestone !== gl.issueBoards.BoardsStore.state.currentBoard.milestone.title;
} }
...@@ -158,18 +159,26 @@ import _ from 'underscore'; ...@@ -158,18 +159,26 @@ import _ from 'underscore';
}, },
isSelectable: function() { isSelectable: function() {
if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') && 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 false;
} }
return true; return true;
}, },
clicked: function(options) { clicked: function(clickEvent) {
const { $el, e } = options; const { $el, e } = clickEvent;
let selected = options.selectedObj; let selected = clickEvent.selectedObj;
var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore; var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
if (!selected) return; if (!selected) return;
if (options.handleClick) {
e.preventDefault();
options.handleClick(selected);
return;
}
page = $('body').attr('data-page'); page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index'); isMRIndex = (page === page && page === 'projects:merge_requests:index');
......
...@@ -6,7 +6,7 @@ import _ from 'underscore'; ...@@ -6,7 +6,7 @@ import _ from 'underscore';
// TODO: remove eventHub hack after code splitting refactor // TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop; window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
function UsersSelect(currentUser, els) { function UsersSelect(currentUser, els, options = {}) {
var $els; var $els;
this.users = this.users.bind(this); this.users = this.users.bind(this);
this.user = this.user.bind(this); this.user = this.user.bind(this);
...@@ -20,6 +20,8 @@ function UsersSelect(currentUser, els) { ...@@ -20,6 +20,8 @@ function UsersSelect(currentUser, els) {
} }
} }
const { handleClick } = options;
$els = $(els); $els = $(els);
if (!els) { if (!els) {
...@@ -442,6 +444,9 @@ function UsersSelect(currentUser, els) { ...@@ -442,6 +444,9 @@ function UsersSelect(currentUser, els) {
} }
if ($el.closest('.add-issues-modal').length) { if ($el.closest('.add-issues-modal').length) {
gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; 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)) { } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
return Issuable.filterResults($dropdown.closest('form')); return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) { } else if ($dropdown.hasClass('js-filter-submit')) {
......
...@@ -5,17 +5,27 @@ export default { ...@@ -5,17 +5,27 @@ export default {
props: { props: {
title: { title: {
type: String, type: String,
required: true, required: false,
}, },
text: { text: {
type: String, type: String,
required: false, required: false,
}, },
hideFooter: {
type: Boolean,
required: false,
default: false,
},
kind: { kind: {
type: String, type: String,
required: false, required: false,
default: 'primary', default: 'primary',
}, },
modalDialogClass: {
type: String,
required: false,
default: '',
},
closeKind: { closeKind: {
type: String, type: String,
required: false, required: false,
...@@ -30,6 +40,11 @@ export default { ...@@ -30,6 +40,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
submitDisabled: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
...@@ -57,43 +72,58 @@ export default { ...@@ -57,43 +72,58 @@ export default {
</script> </script>
<template> <template>
<div <div class="modal-open">
class="modal popup-dialog" <div
role="dialog" class="modal popup-dialog"
tabindex="-1"> role="dialog"
<div class="modal-dialog" role="document"> tabindex="-1"
<div class="modal-content"> >
<div class="modal-header"> <div
<button type="button" :class="modalDialogClass"
class="close" class="modal-dialog"
@click="close" role="document"
aria-label="Close"> >
<span aria-hidden="true">&times;</span> <div class="modal-content">
</button> <div class="modal-header">
<h4 class="modal-title">{{this.title}}</h4> <slot name="header">
</div> <h4 class="modal-title pull-left">
<div class="modal-body"> {{this.title}}
<slot name="body" :text="text"> </h4>
<p>{{text}}</p> <button
</slot> type="button"
</div> class="close pull-right"
<div class="modal-footer"> @click="close"
<button aria-label="Close"
type="button" >
class="btn" <span aria-hidden="true">&times;</span>
:class="btnCancelKindClass" </button>
@click="close"> </slot>
{{ closeButtonLabel }} </div>
</button> <div class="modal-body">
<button <slot name="body" :text="text">
type="button" <p>{{this.text}}</p>
class="btn" </slot>
:class="btnKindClass" </div>
@click="emitSubmit(true)"> <div class="modal-footer" v-if="!hideFooter">
{{ primaryButtonLabel }} <button
</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>
</div> </div>
<div class="modal-backdrop fade in" />
</div> </div>
</template> </template>
...@@ -2,8 +2,10 @@ ...@@ -2,8 +2,10 @@
(function() { (function() {
this.WeightSelect = (function() { this.WeightSelect = (function() {
function WeightSelect() { function WeightSelect(els, options = {}) {
$('.js-weight-select').each(function(i, dropdown) { const $els = $(els || '.js-weight-select');
$els.each(function(i, dropdown) {
var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, updateUrl, updateWeight; var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, updateUrl, updateWeight;
$dropdown = $(dropdown); $dropdown = $(dropdown);
updateUrl = $dropdown.data('issueUpdate'); updateUrl = $dropdown.data('issueUpdate');
...@@ -13,6 +15,13 @@ ...@@ -13,6 +15,13 @@
$value = $block.find('.value'); $value = $block.find('.value');
abilityName = $dropdown.data('ability-name'); abilityName = $dropdown.data('ability-name');
$loading = $block.find('.block-loading').fadeOut(); $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) { updateWeight = function(selected) {
var data; var data;
data = {}; data = {};
...@@ -39,7 +48,7 @@ ...@@ -39,7 +48,7 @@
}; };
return $dropdown.glDropdown({ return $dropdown.glDropdown({
selectable: true, selectable: true,
fieldName: $dropdown.data("field-name"), fieldName,
toggleLabel: function (selected, el) { toggleLabel: function (selected, el) {
return $(el).data("id"); return $(el).data("id");
}, },
...@@ -54,16 +63,21 @@ ...@@ -54,16 +63,21 @@
return ''; return '';
} }
}, },
clicked: function(options) { clicked: function(glDropdownEvt) {
const e = options.e; const e = glDropdownEvt.e;
let selected = options.selectedObj; 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(); return $(dropdown).parents('form').submit();
} else if ($dropdown.is('.js-issuable-form-weight')) { } else if ($dropdown.is('.js-issuable-form-weight')) {
e.preventDefault(); e.preventDefault();
} else { } else {
selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val(); selected = inputField.val();
return updateWeight(selected); return updateWeight(selected);
} }
} }
......
...@@ -59,11 +59,11 @@ ...@@ -59,11 +59,11 @@
&.avatar-tile { &.avatar-tile {
border-radius: 0; border-radius: 0;
border: none; border: 0;
} }
&.avatar-placeholder { &.avatar-placeholder {
border: none; border: 0;
} }
&:not([href]):hover { &:not([href]):hover {
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
.avatar { .avatar {
border-radius: 0; border-radius: 0;
border: none; border: 0;
height: auto; height: auto;
width: 100%; width: 100%;
margin: 0; margin: 0;
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
} }
&.top-block { &.top-block {
border-top: none; border-top: 0;
.container-fluid { .container-fluid {
background-color: inherit; background-color: inherit;
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
&.footer-block { &.footer-block {
margin-top: 0; margin-top: 0;
border-bottom: none; border-bottom: 0;
margin-bottom: -$gl-padding; margin-bottom: -$gl-padding;
} }
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
&.build-content { &.build-content {
background-color: $white-light; background-color: $white-light;
border-top: none; border-top: 0;
} }
} }
...@@ -287,12 +287,12 @@ ...@@ -287,12 +287,12 @@
cursor: pointer; cursor: pointer;
color: $blue-300; color: $blue-300;
z-index: 1; z-index: 1;
border: none; border: 0;
background-color: transparent; background-color: transparent;
&:hover, &:hover,
&:focus { &:focus {
border: none; border: 0;
color: $blue-400; color: $blue-400;
} }
} }
......
...@@ -308,7 +308,7 @@ ...@@ -308,7 +308,7 @@
} }
.btn-clipboard { .btn-clipboard {
border: none; border: 0;
padding: 0 5px; padding: 0 5px;
} }
......
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
.cred { color: $common-red; } .cred { color: $common-red; }
.cgreen { color: $common-green; } .cgreen { color: $common-green; }
.cdark { color: $common-gray-dark; } .cdark { color: $common-gray-dark; }
.text-secondary {
color: $gl-text-color-secondary;
}
.underlined-link { text-decoration: underline; } .underlined-link { text-decoration: underline; }
.hint { font-style: italic; color: $hint-color; } .hint { font-style: italic; color: $hint-color; }
...@@ -28,7 +31,7 @@ ...@@ -28,7 +31,7 @@
pre { pre {
&.clean { &.clean {
background: none; background: none;
border: none; border: 0;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
...@@ -146,7 +149,7 @@ li.note { ...@@ -146,7 +149,7 @@ li.note {
img { max-width: 100%; } img { max-width: 100%; }
.note-title { .note-title {
li { li {
border-bottom: none !important; border-bottom: 0 !important;
} }
} }
} }
...@@ -191,7 +194,7 @@ li.note { ...@@ -191,7 +194,7 @@ li.note {
pre { pre {
background: $white-light; background: $white-light;
border: none; border: 0;
font-size: 12px; font-size: 12px;
} }
} }
...@@ -394,7 +397,7 @@ img.emoji { ...@@ -394,7 +397,7 @@ img.emoji {
} }
.hide-bottom-border { .hide-bottom-border {
border-bottom: none !important; border-bottom: 0 !important;
} }
.gl-accessibility { .gl-accessibility {
......
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
.dropdown-menu-nav { .dropdown-menu-nav {
@include set-visible; @include set-visible;
display: block; display: block;
min-height: 40px;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
width: 100%; width: 100%;
......
...@@ -142,7 +142,7 @@ ...@@ -142,7 +142,7 @@
*/ */
&.blame { &.blame {
table { table {
border: none; border: 0;
margin: 0; margin: 0;
} }
...@@ -150,20 +150,20 @@ ...@@ -150,20 +150,20 @@
border-bottom: 1px solid $blame-border; border-bottom: 1px solid $blame-border;
&:last-child { &:last-child {
border-bottom: none; border-bottom: 0;
} }
} }
td { td {
border-top: none; border-top: 0;
border-bottom: none; border-bottom: 0;
&:first-child { &:first-child {
border-left: none; border-left: 0;
} }
&:last-child { &:last-child {
border-right: none; border-right: 0;
} }
&.blame-commit { &.blame-commit {
......
...@@ -255,7 +255,7 @@ ...@@ -255,7 +255,7 @@
.clear-search { .clear-search {
width: 35px; width: 35px;
background-color: $white-light; background-color: $white-light;
border: none; border: 0;
outline: none; outline: none;
z-index: 1; z-index: 1;
...@@ -418,7 +418,7 @@ ...@@ -418,7 +418,7 @@
.droplab-dropdown .dropdown-menu .filter-dropdown-item { .droplab-dropdown .dropdown-menu .filter-dropdown-item {
.btn { .btn {
border: none; border: 0;
width: 100%; width: 100%;
text-align: left; text-align: left;
padding: 8px 16px; padding: 8px 16px;
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
z-index: 1000; z-index: 1000;
margin-bottom: 0; margin-bottom: 0;
min-height: $header-height; min-height: $header-height;
border: none; border: 0;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
position: fixed; position: fixed;
top: 0; top: 0;
...@@ -169,7 +169,7 @@ ...@@ -169,7 +169,7 @@
.navbar-collapse { .navbar-collapse {
flex: 0 0 auto; flex: 0 0 auto;
border-top: none; border-top: 0;
padding: 0; padding: 0;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
......
.file-content.code { .file-content.code {
border: none; border: 0;
box-shadow: none; box-shadow: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
pre { pre {
padding: 10px 0; padding: 10px 0;
border: none; border: 0;
border-radius: 0; border-radius: 0;
font-family: $monospace_font; font-family: $monospace_font;
font-size: $code_font_size; font-size: $code_font_size;
......
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
} }
&:last-child { &:last-child {
border-bottom: none; border-bottom: 0;
&.bottom { &.bottom {
background: $gray-light; background: $gray-light;
...@@ -92,7 +92,7 @@ ul.unstyled-list { ...@@ -92,7 +92,7 @@ ul.unstyled-list {
} }
ul.unstyled-list > li { ul.unstyled-list > li {
border-bottom: none; border-bottom: 0;
} }
// Generic content list // Generic content list
...@@ -184,7 +184,7 @@ ul.content-list { ...@@ -184,7 +184,7 @@ ul.content-list {
// When dragging a list item // When dragging a list item
&.ui-sortable-helper { &.ui-sortable-helper {
border-bottom: none; border-bottom: 0;
} }
&.list-placeholder { &.list-placeholder {
...@@ -301,7 +301,7 @@ ul.indent-list { ...@@ -301,7 +301,7 @@ ul.indent-list {
} }
> .group-list-tree > .group-row.has-children:first-child { > .group-list-tree > .group-row.has-children:first-child {
border-top: none; border-top: 0;
} }
} }
...@@ -419,7 +419,7 @@ ul.indent-list { ...@@ -419,7 +419,7 @@ ul.indent-list {
padding: 0; padding: 0;
&.has-children { &.has-children {
border-top: none; border-top: 0;
} }
&:first-child { &:first-child {
......
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
margin: 0; margin: 0;
&:last-child { &:last-child {
border-bottom: none; border-bottom: 0;
} }
&.active { &.active {
......
...@@ -42,3 +42,11 @@ body.modal-open { ...@@ -42,3 +42,11 @@ body.modal-open {
width: 98%; width: 98%;
} }
} }
.modal.popup-dialog {
display: block;
}
.modal-body {
background-color: $modal-body-bg;
}
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
margin: 0; margin: 0;
padding: $gl-padding 0; padding: $gl-padding 0;
border: none; border: 0;
&:not(:last-child) { &:not(:last-child) {
border-bottom: 1px solid $white-normal; border-bottom: 1px solid $white-normal;
......
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
.nav-links { .nav-links {
margin-bottom: 0; margin-bottom: 0;
border-bottom: none; border-bottom: 0;
float: left; float: left;
&.wide { &.wide {
...@@ -362,7 +362,7 @@ ...@@ -362,7 +362,7 @@
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
.nav-links { .nav-links {
border-bottom: none; border-bottom: 0;
} }
} }
} }
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
.select2-arrow { .select2-arrow {
background-image: none; background-image: none;
background-color: transparent; background-color: transparent;
border: none; border: 0;
padding-top: 12px; padding-top: 12px;
padding-right: 20px; padding-right: 20px;
font-size: 10px; font-size: 10px;
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
&.container-blank { &.container-blank {
background: none; background: none;
padding: 0; padding: 0;
border: none; border: 0;
} }
} }
} }
...@@ -111,7 +111,7 @@ ...@@ -111,7 +111,7 @@
} }
.block:last-of-type { .block:last-of-type {
border: none; border: 0;
} }
} }
......
...@@ -33,7 +33,7 @@ table { ...@@ -33,7 +33,7 @@ table {
th { th {
background-color: $gray-light; background-color: $gray-light;
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
border-bottom: none; border-bottom: 0;
&.wide { &.wide {
width: 55%; width: 55%;
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
} }
&.text-file .diff-file { &.text-file .diff-file {
border-bottom: none; border-bottom: 0;
} }
} }
...@@ -66,5 +66,5 @@ ...@@ -66,5 +66,5 @@
.discussion .timeline-entry { .discussion .timeline-entry {
margin: 0; margin: 0;
border-right: none; border-right: 0;
} }
...@@ -164,3 +164,36 @@ $pre-border-color: $border-color; ...@@ -164,3 +164,36 @@ $pre-border-color: $border-color;
$table-bg-accent: $gray-light; $table-bg-accent: $gray-light;
$zindex-popover: 900; $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 @@ ...@@ -167,7 +167,7 @@
&.plain-readme { &.plain-readme {
background: none; background: none;
border: none; border: 0;
padding: 0; padding: 0;
margin: 0; margin: 0;
font-size: 14px; font-size: 14px;
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
z-index: 1031; z-index: 1031;
textarea { textarea {
border: none; border: 0;
box-shadow: none; box-shadow: none;
border-radius: 0; border-radius: 0;
color: $black; color: $black;
......
...@@ -473,6 +473,10 @@ ...@@ -473,6 +473,10 @@
white-space: normal; white-space: normal;
} }
.form-section-title {
font-size: 16px;
}
.board-delete-btns { .board-delete-btns {
padding-top: 12px; padding-top: 12px;
border-top: 1px solid $border-color; border-top: 1px solid $border-color;
...@@ -731,3 +735,16 @@ ...@@ -731,3 +735,16 @@
padding-right: 12px; padding-right: 12px;
color: $gl-gray-dark; 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 @@ ...@@ -48,7 +48,7 @@
overflow-x: auto; overflow-x: auto;
font-size: 12px; font-size: 12px;
border-radius: 0; border-radius: 0;
border: none; border: 0;
.bash { .bash {
display: block; display: block;
......
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
pre.commit-message { pre.commit-message {
background: none; background: none;
padding: 0; padding: 0;
border: none; border: 0;
margin: 20px 0; margin: 20px 0;
border-radius: 0; border-radius: 0;
} }
......
.commit-description { .commit-description {
background: none; background: none;
border: none; border: 0;
padding: 0; padding: 0;
margin-top: 10px; margin-top: 10px;
word-break: normal; word-break: normal;
...@@ -251,7 +251,7 @@ ...@@ -251,7 +251,7 @@
word-break: normal; word-break: normal;
pre { pre {
border: none; border: 0;
background: inherit; background: inherit;
padding: 0; padding: 0;
margin: 0; margin: 0;
......
...@@ -80,7 +80,7 @@ ...@@ -80,7 +80,7 @@
.panel { .panel {
.content-block { .content-block {
padding: 24px 0; padding: 24px 0;
border-bottom: none; border-bottom: 0;
position: relative; position: relative;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
...@@ -222,11 +222,11 @@ ...@@ -222,11 +222,11 @@
} }
&:first-child { &:first-child {
border-top: none; border-top: 0;
} }
&:last-child { &:last-child {
border-bottom: none; border-bottom: 0;
} }
.stage-nav-item-cell { .stage-nav-item-cell {
...@@ -290,7 +290,7 @@ ...@@ -290,7 +290,7 @@
border-bottom: 1px solid $gray-darker; border-bottom: 1px solid $gray-darker;
&:last-child { &:last-child {
border-bottom: none; border-bottom: 0;
margin-bottom: 0; margin-bottom: 0;
} }
......
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
table { table {
width: 100%; width: 100%;
font-family: $monospace_font; font-family: $monospace_font;
border: none; border: 0;
border-collapse: separate; border-collapse: separate;
margin: 0; margin: 0;
padding: 0; padding: 0;
...@@ -105,7 +105,7 @@ ...@@ -105,7 +105,7 @@
.new_line { .new_line {
@include user-select(none); @include user-select(none);
margin: 0; margin: 0;
border: none; border: 0;
padding: 0 5px; padding: 0 5px;
border-right: 1px solid; border-right: 1px solid;
text-align: right; text-align: right;
...@@ -133,7 +133,7 @@ ...@@ -133,7 +133,7 @@
display: block; display: block;
margin: 0; margin: 0;
padding: 0 1.5em; padding: 0 1.5em;
border: none; border: 0;
position: relative; position: relative;
&.parallel { &.parallel {
...@@ -359,7 +359,7 @@ ...@@ -359,7 +359,7 @@
cursor: pointer; cursor: pointer;
&:first-child { &:first-child {
border-left: none; border-left: 0;
} }
&:hover { &:hover {
...@@ -388,7 +388,7 @@ ...@@ -388,7 +388,7 @@
.file-content .diff-file { .file-content .diff-file {
margin: 0; margin: 0;
border: none; border: 0;
} }
.diff-wrap-lines .line_content { .diff-wrap-lines .line_content {
...@@ -400,7 +400,7 @@ ...@@ -400,7 +400,7 @@
} }
.files-changed { .files-changed {
border-bottom: none; border-bottom: 0;
} }
.diff-stats-summary-toggler { .diff-stats-summary-toggler {
......
...@@ -3,13 +3,13 @@ ...@@ -3,13 +3,13 @@
border-top: 1px solid $border-color; border-top: 1px solid $border-color;
border-right: 1px solid $border-color; border-right: 1px solid $border-color;
border-left: 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; border-radius: $border-radius-small $border-radius-small 0 0;
background: $gray-normal; background: $gray-normal;
} }
#editor { #editor {
border: none; border: 0;
border-radius: 0; border-radius: 0;
height: 500px; height: 500px;
margin: 0; margin: 0;
...@@ -171,7 +171,7 @@ ...@@ -171,7 +171,7 @@
width: 100%; width: 100%;
margin: 5px 0; margin: 5px 0;
padding: 0; padding: 0;
border-left: none; border-left: 0;
} }
} }
......
...@@ -117,7 +117,7 @@ ...@@ -117,7 +117,7 @@
} }
.no-btn { .no-btn {
border: none; border: 0;
background: none; background: none;
outline: none; outline: none;
width: 100%; width: 100%;
...@@ -293,11 +293,11 @@ ...@@ -293,11 +293,11 @@
} }
.folder-row { .folder-row {
border-left: none; border-left: 0;
border-right: none; border-right: 0;
@media (min-width: $screen-sm-max) { @media (min-width: $screen-sm-max) {
border-top: none; border-top: 0;
} }
} }
......
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
} }
pre { pre {
border: none; border: 0;
background: $gray-light; background: $gray-light;
border-radius: 0; border-radius: 0;
color: $events-pre-color; color: $events-pre-color;
...@@ -128,14 +128,14 @@ ...@@ -128,14 +128,14 @@
} }
} }
&:last-child { border: none; } &:last-child { border: 0; }
.event_commits { .event_commits {
li { li {
&.commit { &.commit {
background: transparent; background: transparent;
padding: 0; padding: 0;
border: none; border: 0;
.commit-row-title { .commit-row-title {
font-size: $gl-font-size; font-size: $gl-font-size;
......
...@@ -217,7 +217,7 @@ ...@@ -217,7 +217,7 @@
margin-top: $gl-padding; margin-top: $gl-padding;
.pipeline-quota { .pipeline-quota {
border-top: none; border-top: 0;
} }
} }
......
...@@ -79,7 +79,7 @@ ...@@ -79,7 +79,7 @@
.title { .title {
padding: 0; padding: 0;
margin-bottom: 16px; margin-bottom: 16px;
border-bottom: none; border-bottom: 0;
} }
.btn-edit { .btn-edit {
...@@ -164,7 +164,7 @@ ...@@ -164,7 +164,7 @@
} }
&:last-child { &:last-child {
border: none; border: 0;
} }
span { span {
...@@ -338,7 +338,7 @@ ...@@ -338,7 +338,7 @@
.block { .block {
width: $gutter_collapsed_width - 2px; width: $gutter_collapsed_width - 2px;
padding: 15px 0 0; padding: 15px 0 0;
border-bottom: none; border-bottom: 0;
overflow: hidden; overflow: hidden;
} }
...@@ -399,7 +399,7 @@ ...@@ -399,7 +399,7 @@
} }
.btn-clipboard { .btn-clipboard {
border: none; border: 0;
color: $issuable-sidebar-color; color: $issuable-sidebar-color;
&:hover { &:hover {
......
...@@ -139,7 +139,7 @@ ...@@ -139,7 +139,7 @@
border-left: 1px solid $border-color; border-left: 1px solid $border-color;
&:first-of-type { &:first-of-type {
border-left: none; border-left: 0;
border-top-left-radius: $border-radius-default; border-top-left-radius: $border-radius-default;
} }
...@@ -165,7 +165,7 @@ ...@@ -165,7 +165,7 @@
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
a { a {
border: none; border: 0;
border-bottom: 2px solid $link-underline-blue; border-bottom: 2px solid $link-underline-blue;
margin-right: 0; margin-right: 0;
color: $black; color: $black;
......
...@@ -262,7 +262,7 @@ $colors: ( ...@@ -262,7 +262,7 @@ $colors: (
.editor { .editor {
pre { pre {
height: 350px; height: 350px;
border: none; border: 0;
border-radius: 0; border-radius: 0;
margin-bottom: 0; margin-bottom: 0;
} }
......
...@@ -750,7 +750,7 @@ ...@@ -750,7 +750,7 @@
} }
.unapprove-btn { .unapprove-btn {
border: none; border: 0;
background: transparent; background: transparent;
cursor: pointer; cursor: pointer;
...@@ -795,7 +795,9 @@ ...@@ -795,7 +795,9 @@
} }
.mr-widget-code-quality { .mr-widget-code-quality {
padding-top: $gl-padding-top; .ci-status-icon-warning svg {
fill: $theme-gray-600;
}
.code-quality-container { .code-quality-container {
border-top: 1px solid $gray-darker; border-top: 1px solid $gray-darker;
...@@ -806,15 +808,25 @@ ...@@ -806,15 +808,25 @@
.mr-widget-code-quality-list { .mr-widget-code-quality-list {
list-style: none; list-style: none;
padding: 4px 36px; padding: 0 12px;
margin: 0; margin: 0;
line-height: $code_line_height; 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; color: $green-500;
} }
li.failed { .failed {
color: $red-500; color: $red-500;
} }
} }
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
.discussion { .discussion {
.new-note { .new-note {
margin: 0; margin: 0;
border: none; border: 0;
} }
} }
...@@ -106,12 +106,12 @@ ...@@ -106,12 +106,12 @@
background-color: $orange-100; background-color: $orange-100;
border-radius: $border-radius-default $border-radius-default 0 0; border-radius: $border-radius-default $border-radius-default 0 0;
border: 1px solid $border-gray-normal; border: 1px solid $border-gray-normal;
border-bottom: none; border-bottom: 0;
padding: 3px 12px; padding: 3px 12px;
margin: auto; margin: auto;
align-items: center; align-items: center;
+ .md-area { .md-area {
border-top-left-radius: 0; border-top-left-radius: 0;
border-top-right-radius: 0; border-top-right-radius: 0;
} }
......
...@@ -331,7 +331,7 @@ ul.notes { ...@@ -331,7 +331,7 @@ ul.notes {
td { td {
border: 1px solid $white-normal; border: 1px solid $white-normal;
border-left: none; border-left: 0;
&.notes_line { &.notes_line {
vertical-align: middle; vertical-align: middle;
...@@ -666,7 +666,7 @@ ul.notes { ...@@ -666,7 +666,7 @@ ul.notes {
.timeline-entry-inner { .timeline-entry-inner {
padding-left: $gl-padding; padding-left: $gl-padding;
padding-right: $gl-padding; padding-right: $gl-padding;
border-bottom: none; border-bottom: 0;
} }
} }
} }
...@@ -679,7 +679,7 @@ ul.notes { ...@@ -679,7 +679,7 @@ ul.notes {
padding: 90px 0; padding: 90px 0;
&.discussion-locked { &.discussion-locked {
border: none; border: 0;
background-color: $white-light; background-color: $white-light;
} }
...@@ -759,7 +759,7 @@ ul.notes { ...@@ -759,7 +759,7 @@ ul.notes {
top: 0; top: 0;
padding: 0; padding: 0;
background-color: transparent; background-color: transparent;
border: none; border: 0;
outline: 0; outline: 0;
color: $gray-darkest; color: $gray-darkest;
transition: color $general-hover-transition-duration $general-hover-transition-curve; transition: color $general-hover-transition-duration $general-hover-transition-curve;
......
...@@ -307,7 +307,7 @@ ...@@ -307,7 +307,7 @@
&::after { &::after {
content: ''; content: '';
width: 0; width: 0;
border: none; border: 0;
} }
} }
} }
...@@ -323,7 +323,7 @@ ...@@ -323,7 +323,7 @@
.pipeline-actions { .pipeline-actions {
@include new-style-dropdown; @include new-style-dropdown;
border-bottom: none; border-bottom: 0;
} }
.tab-pane { .tab-pane {
...@@ -353,7 +353,7 @@ ...@@ -353,7 +353,7 @@
} }
.build-log { .build-log {
border: none; border: 0;
line-height: initial; line-height: initial;
} }
} }
...@@ -414,13 +414,13 @@ ...@@ -414,13 +414,13 @@
// Remove right connecting horizontal line from first build in last stage // Remove right connecting horizontal line from first build in last stage
&:first-child { &:first-child {
&::after { &::after {
border: none; border: 0;
} }
} }
// Remove right curved connectors from all builds in last stage // Remove right curved connectors from all builds in last stage
&:not(:first-child) { &:not(:first-child) {
&::after { &::after {
border: none; border: 0;
} }
} }
// Remove opposite curve // Remove opposite curve
...@@ -438,7 +438,7 @@ ...@@ -438,7 +438,7 @@
// Remove left curved connectors from all builds in first stage // Remove left curved connectors from all builds in first stage
&:not(:first-child) { &:not(:first-child) {
&::before { &::before {
border: none; border: 0;
} }
} }
// Remove opposite curve // Remove opposite curve
...@@ -544,7 +544,7 @@ ...@@ -544,7 +544,7 @@
.dropdown-menu-toggle { .dropdown-menu-toggle {
background-color: transparent; background-color: transparent;
border: none; border: 0;
padding: 0; padding: 0;
&:focus { &:focus {
...@@ -978,7 +978,7 @@ a.linked-pipeline-mini-item { ...@@ -978,7 +978,7 @@ a.linked-pipeline-mini-item {
.terminal-container { .terminal-container {
.content-block { .content-block {
border-bottom: none; border-bottom: 0;
} }
#terminal { #terminal {
......
...@@ -113,7 +113,7 @@ ...@@ -113,7 +113,7 @@
li { li {
padding: 3px 0; padding: 3px 0;
border: none; border: 0;
} }
} }
......
...@@ -88,7 +88,7 @@ ...@@ -88,7 +88,7 @@
.project-feature-settings { .project-feature-settings {
background: $gray-lighter; background: $gray-lighter;
border-top: none; border-top: 0;
margin-bottom: 16px; margin-bottom: 16px;
} }
...@@ -136,7 +136,7 @@ ...@@ -136,7 +136,7 @@
.project-feature-toggle { .project-feature-toggle {
position: relative; position: relative;
border: none; border: 0;
outline: 0; outline: 0;
display: block; display: block;
width: 100px; width: 100px;
...@@ -491,7 +491,7 @@ a.deploy-project-label { ...@@ -491,7 +491,7 @@ a.deploy-project-label {
flex: 1; flex: 1;
padding: 0; padding: 0;
background: transparent; background: transparent;
border: none; border: 0;
line-height: 34px; line-height: 34px;
margin: 0; margin: 0;
...@@ -1031,7 +1031,7 @@ a.allowed-to-push { ...@@ -1031,7 +1031,7 @@ a.allowed-to-push {
margin: 0; margin: 0;
border-radius: 0 0 1px 1px; border-radius: 0 0 1px 1px;
padding: 20px 0; padding: 20px 0;
border: none; border: 0;
} }
.table-bordered { .table-bordered {
...@@ -1207,7 +1207,7 @@ a.allowed-to-push { ...@@ -1207,7 +1207,7 @@ a.allowed-to-push {
table-layout: fixed; table-layout: fixed;
&.table-responsive { &.table-responsive {
border: none; border: 0;
} }
.variable-key { .variable-key {
......
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
} }
.bordered-box.content-block { .bordered-box.content-block {
border: none; border: 0;
padding: 20px 0; padding: 20px 0;
justify-content: left; justify-content: left;
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
} }
.modal-header { .modal-header {
border-bottom: none; border-bottom: 0;
} }
.modal-body { .modal-body {
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
} }
.modal-footer { .modal-footer {
border-top: none; border-top: 0;
} }
} }
...@@ -139,7 +139,7 @@ ...@@ -139,7 +139,7 @@
margin: 0 -25px; margin: 0 -25px;
padding: 0; padding: 0;
overflow: initial; overflow: initial;
border-bottom: none; border-bottom: 0;
} }
.btn { .btn {
......
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
.monaco-editor.vs { .monaco-editor.vs {
.current-line { .current-line {
border: none; border: 0;
background: $well-light-border; background: $well-light-border;
} }
...@@ -139,7 +139,7 @@ ...@@ -139,7 +139,7 @@
&.active { &.active {
background: $white-light; background: $white-light;
border-bottom: none; border-bottom: 0;
} }
a { a {
...@@ -181,7 +181,7 @@ ...@@ -181,7 +181,7 @@
&.tabs-divider { &.tabs-divider {
width: 100%; width: 100%;
background-color: $white-light; background-color: $white-light;
border-right: none; border-right: 0;
border-top-right-radius: 2px; border-top-right-radius: 2px;
} }
} }
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
&:last-child { &:last-child {
border-bottom: none; border-bottom: 0;
} }
} }
...@@ -57,7 +57,7 @@ input[type="checkbox"]:hover { ...@@ -57,7 +57,7 @@ input[type="checkbox"]:hover {
} }
.search-input { .search-input {
border: none; border: 0;
font-size: 14px; font-size: 14px;
padding: 0 20px 0 0; padding: 0 20px 0 0;
margin-left: 5px; margin-left: 5px;
......
...@@ -141,7 +141,7 @@ ...@@ -141,7 +141,7 @@
} }
pre { pre {
border: none; border: 0;
background: $gray-light; background: $gray-light;
border-radius: 0; border-radius: 0;
color: $todo-body-pre-color; color: $todo-body-pre-color;
......
...@@ -252,7 +252,7 @@ ...@@ -252,7 +252,7 @@
margin-top: 20px; margin-top: 20px;
padding: 0; padding: 0;
border-top: 1px solid $white-dark; border-top: 1px solid $white-dark;
border-bottom: none; border-bottom: 0;
} }
.commit-stats li { .commit-stats li {
......
...@@ -127,6 +127,7 @@ module IssuableActions ...@@ -127,6 +127,7 @@ module IssuableActions
:milestone_id, :milestone_id,
:state_event, :state_event,
:subscription_event, :subscription_event,
:weight,
label_ids: [], label_ids: [],
add_label_ids: [], add_label_ids: [],
remove_label_ids: [] remove_label_ids: []
......
...@@ -156,7 +156,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -156,7 +156,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
message << " Create a GitLab account first, and then connect it to your #{label} account." message << " Create a GitLab account first, and then connect it to your #{label} account."
end end
flash[:notice] = message flash[:alert] = message
redirect_to new_user_session_path redirect_to new_user_session_path
end end
......
...@@ -22,17 +22,6 @@ module BoardsHelper ...@@ -22,17 +22,6 @@ module BoardsHelper
project_issues_path(@project) project_issues_path(@project)
end 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 def board_base_url
project_boards_path(@project) project_boards_path(@project)
end 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 ...@@ -30,6 +30,7 @@ class License < ActiveRecord::Base
related_issues related_issues
repository_mirrors repository_mirrors
repository_size_limit repository_size_limit
scoped_issue_board
].freeze ].freeze
EEP_FEATURES = EES_FEATURES + %i[ EEP_FEATURES = EES_FEATURES + %i[
...@@ -70,7 +71,6 @@ class License < ActiveRecord::Base ...@@ -70,7 +71,6 @@ class License < ActiveRecord::Base
group_webhooks group_webhooks
issuable_default_templates issuable_default_templates
issue_board_focus_mode issue_board_focus_mode
issue_board_milestone
issue_weights issue_weights
jenkins_integration jenkins_integration
merge_request_approvers merge_request_approvers
......
...@@ -227,10 +227,6 @@ class Namespace < ActiveRecord::Base ...@@ -227,10 +227,6 @@ class Namespace < ActiveRecord::Base
feature_available?(:multiple_issue_boards) feature_available?(:multiple_issue_boards)
end end
def issue_board_milestone_available?(user = nil)
feature_available?(:issue_board_milestone)
end
private private
def refresh_access_of_projects_invited_groups def refresh_access_of_projects_invited_groups
......
...@@ -1600,10 +1600,6 @@ class Project < ActiveRecord::Base ...@@ -1600,10 +1600,6 @@ class Project < ActiveRecord::Base
feature_available?(:multiple_issue_boards, user) feature_available?(:multiple_issue_boards, user)
end end
def issue_board_milestone_available?(user = nil)
feature_available?(:issue_board_milestone, user)
end
def full_path_was def full_path_was
File.join(namespace.full_path, previous_changes['path'].first) File.join(namespace.full_path, previous_changes['path'].first)
end end
...@@ -1684,6 +1680,10 @@ class Project < ActiveRecord::Base ...@@ -1684,6 +1680,10 @@ class Project < ActiveRecord::Base
Gitlab::GlRepository.gl_repository(self, is_wiki) Gitlab::GlRepository.gl_repository(self, is_wiki)
end end
def reference_counter(wiki: false)
Gitlab::ReferenceCounter.new(gl_repository(is_wiki: wiki))
end
private private
def storage def storage
...@@ -1702,11 +1702,11 @@ class Project < ActiveRecord::Base ...@@ -1702,11 +1702,11 @@ class Project < ActiveRecord::Base
end end
def repo_reference_count def repo_reference_count
Gitlab::ReferenceCounter.new(gl_repository(is_wiki: false)).value reference_counter.value
end end
def wiki_reference_count def wiki_reference_count
Gitlab::ReferenceCounter.new(gl_repository(is_wiki: true)).value reference_counter(wiki: true).value
end end
def check_repository_absence! def check_repository_absence!
......
...@@ -150,7 +150,7 @@ class ProjectWiki ...@@ -150,7 +150,7 @@ class ProjectWiki
end end
def repository 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 end
def default_branch def default_branch
......
...@@ -18,10 +18,9 @@ class Repository ...@@ -18,10 +18,9 @@ class Repository
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include Elastic::RepositoriesSearch include Elastic::RepositoriesSearch
include RepositoryMirroring
prepend EE::Repository 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 delegate :ref_name_for_sha, to: :raw_repository
...@@ -79,11 +78,12 @@ class Repository ...@@ -79,11 +78,12 @@ class Repository
end end
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 @full_path = full_path
@disk_path = disk_path || full_path @disk_path = disk_path || full_path
@project = project @project = project
@commit_cache = {} @commit_cache = {}
@is_wiki = is_wiki
end end
def ==(other) def ==(other)
...@@ -1020,19 +1020,6 @@ class Repository ...@@ -1020,19 +1020,6 @@ class Repository
run_git(args).first.lines.map(&:strip) run_git(args).first.lines.map(&:strip)
end 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) 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) gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags)
end end
...@@ -1202,7 +1189,7 @@ class Repository ...@@ -1202,7 +1189,7 @@ class Repository
end end
def initialize_raw_repository 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 end
def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset) def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
......
...@@ -197,11 +197,19 @@ class MergeRequestEntity < IssuableEntity ...@@ -197,11 +197,19 @@ class MergeRequestEntity < IssuableEntity
path: 'codeclimate.json') path: 'codeclimate.json')
end 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| 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, raw_project_build_artifacts_url(merge_request.target_project,
merge_request.base_codeclimate_artifact, merge_request.base_codeclimate_artifact,
path: 'codeclimate.json') path: 'codeclimate.json')
end 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 end
private private
......
...@@ -6,5 +6,10 @@ module Boards ...@@ -6,5 +6,10 @@ module Boards
def initialize(parent, user, params = {}) def initialize(parent, user, params = {})
@parent, @current_user, @params = parent, user, params.dup @parent, @current_user, @params = parent, user, params.dup
end end
def set_assignee
assignee = User.find_by(id: params.delete(:assignee_id))
params.merge!(assignee: assignee)
end
end end
end end
...@@ -13,6 +13,7 @@ module Boards ...@@ -13,6 +13,7 @@ module Boards
end end
def create_board! def create_board!
set_assignee
board = parent.boards.create(params) board = parent.boards.create(params)
if board.persisted? if board.persisted?
......
...@@ -10,11 +10,25 @@ module Boards ...@@ -10,11 +10,25 @@ module Boards
end end
def execute def execute
create_issue(params.merge(label_ids: [list.label_id])) create_issue(creation_params)
end end
private 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 def board
@board ||= parent.boards.find(params.delete(:board_id)) @board ||= parent.boards.find(params.delete(:board_id))
end end
......
module Boards module Boards
class UpdateService < Boards::BaseService class UpdateService < Boards::BaseService
def execute(board) 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) board.update(params)
end 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. # `#try_obtain_lease` takes a block which will be run if it was able to
# Implement `#lease_timeout` to configure the timeout for the exclusive lease. # obtain the lease. Implement `#lease_timeout` to configure the timeout
# Optionally override `#lease_key` to set the lease key, it defaults to the class name with underscores. # for the exclusive lease. Optionally override `#lease_key` to set the
# lease key, it defaults to the class name with underscores.
# #
module ExclusiveLeaseGuard module ExclusiveLeaseGuard
extend ActiveSupport::Concern 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 def try_obtain_lease
lease = exclusive_lease.try_obtain lease = exclusive_lease.try_obtain
unless lease 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 return
end end
...@@ -40,6 +29,15 @@ module ExclusiveLeaseGuard ...@@ -40,6 +29,15 @@ module ExclusiveLeaseGuard
@lease ||= Gitlab::ExclusiveLease.new(lease_key, timeout: lease_timeout) @lease ||= Gitlab::ExclusiveLease.new(lease_key, timeout: lease_timeout)
end 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) def release_lease(uuid)
Gitlab::ExclusiveLease.cancel(lease_key, uuid) Gitlab::ExclusiveLease.cancel(lease_key, uuid)
end end
...@@ -47,4 +45,8 @@ module ExclusiveLeaseGuard ...@@ -47,4 +45,8 @@ module ExclusiveLeaseGuard
def renew_lease! def renew_lease!
exclusive_lease.renew exclusive_lease.renew
end end
def log_error(message, extra_args = {})
logger.error(messages)
end
end end
...@@ -4,6 +4,7 @@ module Geo ...@@ -4,6 +4,7 @@ module Geo
EmptyCloneUrlPrefixError = Class.new(StandardError) EmptyCloneUrlPrefixError = Class.new(StandardError)
class BaseSyncService class BaseSyncService
include ExclusiveLeaseGuard
include ::Gitlab::Geo::ProjectLogHelpers include ::Gitlab::Geo::ProjectLogHelpers
class << self class << self
...@@ -31,6 +32,10 @@ module Geo ...@@ -31,6 +32,10 @@ module Geo
@lease_key ||= "#{LEASE_KEY_PREFIX}:#{type}:#{project.id}" @lease_key ||= "#{LEASE_KEY_PREFIX}:#{type}:#{project.id}"
end end
def lease_timeout
LEASE_TIMEOUT
end
def primary_ssh_path_prefix def primary_ssh_path_prefix
@primary_ssh_path_prefix ||= Gitlab::Geo.primary_node.clone_url_prefix.tap do |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? raise EmptyCloneUrlPrefixError, 'Missing clone_url_prefix in the primary node' unless prefix.present?
...@@ -89,24 +94,6 @@ module Geo ...@@ -89,24 +94,6 @@ module Geo
@registry ||= Geo::ProjectRegistry.find_or_initialize_by(project_id: project.id) @registry ||= Geo::ProjectRegistry.find_or_initialize_by(project_id: project.id)
end 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) def update_registry(started_at: nil, finished_at: nil)
return unless started_at || finished_at return unless started_at || finished_at
......
...@@ -21,7 +21,9 @@ module Geo ...@@ -21,7 +21,9 @@ module Geo
log_info("Finished repository sync", log_info("Finished repository sync",
update_delay_s: update_delay_in_seconds, update_delay_s: update_delay_in_seconds,
download_time_s: download_time_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) log_error('Error syncing repository', e)
rescue Gitlab::Git::Repository::NoRepository => e rescue Gitlab::Git::Repository::NoRepository => e
log_error('Invalid repository', e) log_error('Invalid repository', e)
......
...@@ -21,6 +21,7 @@ module Geo ...@@ -21,6 +21,7 @@ module Geo
update_delay_s: update_delay_in_seconds, update_delay_s: update_delay_in_seconds,
download_time_s: download_time_in_seconds) download_time_s: download_time_in_seconds)
rescue Gitlab::Git::Repository::NoRepository, rescue Gitlab::Git::Repository::NoRepository,
Gitlab::Git::RepositoryMirroring::RemoteError,
Gitlab::Shell::Error, Gitlab::Shell::Error,
ProjectWiki::CouldNotCreateWikiError, ProjectWiki::CouldNotCreateWikiError,
Geo::EmptyCloneUrlPrefixError => e Geo::EmptyCloneUrlPrefixError => e
......
...@@ -44,7 +44,7 @@ module Projects ...@@ -44,7 +44,7 @@ module Projects
else else
clone_repository clone_repository
end end
rescue Gitlab::Shell::Error => e rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError => e
# Expire cache to prevent scenarios such as: # Expire cache to prevent scenarios such as:
# 1. First import failed, but the repo was imported successfully, so +exists?+ returns true # 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 # 2. Retried import, repo is broken or not imported but +exists?+ still returns true
......
...@@ -19,7 +19,7 @@ module Projects ...@@ -19,7 +19,7 @@ module Projects
update_branches update_branches
success success
rescue Gitlab::Shell::Error, UpdateError => e rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError, UpdateError => e
error(e.message) error(e.message)
end end
......
...@@ -7,11 +7,10 @@ module Projects ...@@ -7,11 +7,10 @@ module Projects
end end
def execute(new_repository_storage_key) def execute(new_repository_storage_key)
new_storage_path = Gitlab.config.repositories.storages[new_repository_storage_key]['path'] result = mirror_repository(new_repository_storage_key)
result = move_storage(project.disk_path, new_storage_path)
if project.wiki.repository_exists? 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 end
if result if result
...@@ -25,8 +24,18 @@ module Projects ...@@ -25,8 +24,18 @@ module Projects
private private
def move_storage(project_path, new_storage_path) def mirror_repository(new_storage_key, wiki: false)
gitlab_shell.mv_storage(project.repository_storage_path, project_path, new_storage_path) 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 end
def mark_old_paths_for_archive def mark_old_paths_for_archive
...@@ -53,5 +62,17 @@ module Projects ...@@ -53,5 +62,17 @@ module Projects
def moved_path(path) def moved_path(path)
"#{path}+#{project.id}+moved+#{Time.now.to_i}" "#{path}+#{project.id}+moved+#{Time.now.to_i}"
end 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
end end
...@@ -42,4 +42,4 @@ ...@@ -42,4 +42,4 @@
.panel.panel-default .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 @@ ...@@ -5,63 +5,42 @@
%boards-selector{ "inline-template" => true, %boards-selector{ "inline-template" => true,
":current-board" => current_board_json, ":current-board" => current_board_json,
"milestone-path" => milestones_filter_path(milestone_filter_opts) } "milestone-path" => milestones_filter_path(milestone_filter_opts) }
.dropdown %span.boards-selector-wrapper
%button.dropdown-menu-toggle{ "v-on:click" => "loadBoards", .dropdown
data: { toggle: "dropdown" } } %button.dropdown-menu-toggle{ "v-on:click" => "loadBoards",
{{ board.name }} data: { toggle: "dropdown" } }
= icon("chevron-down") {{ board.name }}
.dropdown-menu{ ":class" => "{ 'is-loading': loading }" } = icon("chevron-down")
.dropdown-title .dropdown-menu{ ":class" => "{ 'is-loading': loading }" }
%button.dropdown-title-button.dropdown-menu-back{ type: "button", .dropdown-content
aria: { label: "Go back" }, %ul{ "v-if" => "!loading" }
"v-on:click.stop.prevent" => "showPage('')", %li{ "v-for" => "board in boards" }
"v-if" => "currentPage !== ''" } %a{ ":href" => "'#{board_base_url}/' + board.id" }
= icon("arrow-left") {{ board.name }}
{{ title }} - if !multiple_boards_available? && current_board_parent.boards.size > 1
%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)
%li %li
%a{ "href" => "#", "v-on:click.stop.prevent" => "showPage('new')" } .small.unclickable
Create new board Some of your boards are hidden, activate a license to see them again.
%li .dropdown-loading{ "v-if" => "loading" }
%a{ "href" => "#", "v-on:click.stop.prevent" => "showPage('edit')" } = icon("spin spinner")
Edit board name
- if parent.issue_board_milestone_available?(current_user) - if can?(current_user, :admin_board, parent)
%li .dropdown-footer
%a{ "href" => "#", "v-on:click.stop.prevent" => "showPage('milestone')" } %ul.dropdown-footer-list
Edit board milestone - if parent.feature_available?(:multiple_issue_boards)
%li{ "v-if" => "showDelete" } %li
%a{ "href" => "#", "v-on:click.stop.prevent" => "showPage('delete')" } %a{ "href" => "#", "v-on:click.prevent" => "showPage('new')" }
%span.text-danger Create new board
Delete 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 @@ ...@@ -123,7 +123,10 @@
= icon('times') = icon('times')
.filter-dropdown-container .filter-dropdown-container
- if type == :boards - 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 .dropdown.prepend-left-10#js-add-list
%button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data } %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data }
Add list Add list
......
...@@ -15,5 +15,10 @@ module Geo ...@@ -15,5 +15,10 @@ module Geo
def lease_timeout def lease_timeout
LEASE_TIMEOUT LEASE_TIMEOUT
end 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
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