Read about our upcoming Code of Conduct on this issue

Commit 129b4eb1 authored by Kamil Trzcinski's avatar Kamil Trzcinski
Browse files

Merge branch 'zj/gitlab-ce-zj-auto-devops-table' into 37158-autodevops-banner

......@@ -40,6 +40,7 @@ stages:
- test
- post-test
- pages
- post-cleanup
# Predefined scopes
.dedicated-runner: &dedicated-runner
......@@ -153,8 +154,7 @@ stages:
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
# Trigger a package build on omnibus-gitlab repository
# Trigger a package build in omnibus-gitlab repository
build-package:
image: ruby:2.3-alpine
before_script: []
......@@ -166,11 +166,47 @@ build-package:
cache: {}
when: manual
script:
- scripts/trigger-build
- scripts/trigger-build-omnibus
only:
- //@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee
# Review docs base
.review-docs: &review-docs
image: ruby:2.4-alpine
before_script: []
services: []
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
cache: {}
when: manual
only:
- branches
# Trigger a docs build in gitlab-docs
# Useful to preview the docs changes live
review-docs-deploy:
<<: *review-docs
stage: build
environment:
name: review-docs/$CI_COMMIT_REF_NAME
on_stop: review-docs-cleanup
script:
- gem install gitlab --no-doc
- scripts/trigger-build-docs deploy
# Cleanup remote environment of gitlab-docs
review-docs-cleanup:
<<: *review-docs
stage: post-cleanup
environment:
name: review-docs/$CI_COMMIT_REF_NAME
action: stop
script:
- gem install gitlab --no-doc
- scripts/trigger-build-docs cleanup
# Retrieve knapsack and rspec_flaky reports
retrieve-tests-metadata:
<<: *tests-metadata-state
......
......@@ -5,7 +5,7 @@ By submitting code as an individual you agree to the
By submitting code as an entity you agree to the
[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md).
_This notice should stay as the first item in the CONTRIBUTING.MD file._
_This notice should stay as the first item in the CONTRIBUTING.md file._
---
......@@ -21,7 +21,7 @@ _This notice should stay as the first item in the CONTRIBUTING.MD file._
- [Workflow labels](#workflow-labels)
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~CI, ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-ci-discussion-edge-platform-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-ci-discussion-edge-platform-etc)
- [Priority labels (~Deliverable and ~Stretch)](#priority-labels-deliverable-and-stretch)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design--ui-elements)
......@@ -115,7 +115,7 @@ Most issues will have labels for at least one of the following:
- Type: ~"feature proposal", ~bug, ~customer, etc.
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
- Team: ~CI, ~Discussion, ~Edge, ~Platform, etc.
- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.
- Priority: ~Deliverable, ~Stretch
All labels, their meaning and priority are defined on the
......@@ -157,13 +157,13 @@ Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api,
Subject labels are always all-lowercase.
### Team labels (~CI, ~Discussion, ~Edge, ~Platform, etc.)
### Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)
Team labels specify what team is responsible for this issue.
Assigning a team label makes sure issues get the attention of the appropriate
people.
The current team labels are ~Build, ~CI, ~Discussion, ~Documentation, ~Edge,
The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Edge,
~Geo, ~Gitaly, ~Platform, ~Prometheus, ~Release, and ~"UX".
The descriptions on the [labels page][labels-page] explain what falls under the
......@@ -217,11 +217,11 @@ After adding the ~"Accepting Merge Requests" label, we try to estimate the
[weight](#issue-weight) of the issue. We use issue weight to let contributors
know how difficult the issue is. Additionally:
- We advertise [~"Accepting Merge Requests" issues with weight < 5][up-for-grabs]
- We advertise ["Accepting Merge Requests" issues with weight < 5][up-for-grabs]
as suitable for people that have never contributed to GitLab before on the
[Up For Grabs campaign](http://up-for-grabs.net)
- We encourage people that have never contributed to any open source project to
look for [~"Accepting Merge Requests" issues with a weight of 1][firt-timers]
look for ["Accepting Merge Requests" issues with a weight of 1][firt-timers]
[up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests&scope=all&sort=weight_asc&state=opened
[firt-timers]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=Accepting+Merge+Requests&scope=all&sort=upvotes_desc&state=opened&weight=1
......
......@@ -6,7 +6,8 @@ const Api = {
namespacesPath: '/api/:version/namespaces.json',
groupProjectsPath: '/api/:version/groups/:id/projects.json',
projectsPath: '/api/:version/projects.json',
labelsPath: '/:namespace_path/:project_path/labels',
projectLabelsPath: '/:namespace_path/:project_path/labels',
groupLabelsPath: '/groups/:namespace_path/labels',
licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key',
gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key',
......@@ -74,9 +75,16 @@ const Api = {
},
newLabel(namespacePath, projectPath, data, callback) {
const url = Api.buildUrl(Api.labelsPath)
.replace(':namespace_path', namespacePath)
.replace(':project_path', projectPath);
let url;
if (projectPath) {
url = Api.buildUrl(Api.projectLabelsPath)
.replace(':namespace_path', namespacePath)
.replace(':project_path', projectPath);
} else {
url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
}
return $.ajax({
url,
type: 'POST',
......
......@@ -53,7 +53,8 @@ $(() => {
data: {
state: Store.state,
loading: true,
endpoint: $boardApp.dataset.endpoint,
boardsEndpoint: $boardApp.dataset.boardsEndpoint,
listsEndpoint: $boardApp.dataset.listsEndpoint,
boardId: $boardApp.dataset.boardId,
disabled: $boardApp.dataset.disabled === 'true',
issueLinkBase: $boardApp.dataset.issueLinkBase,
......@@ -68,7 +69,13 @@ $(() => {
},
},
created () {
gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId);
gl.boardService = new BoardService({
boardsEndpoint: this.boardsEndpoint,
listsEndpoint: this.listsEndpoint,
bulkUpdatePath: this.bulkUpdatePath,
boardId: this.boardId,
});
Store.rootPath = this.boardsEndpoint;
this.filterManager = new FilteredSearchBoards(Store.filter, true);
this.filterManager.setup();
......@@ -112,19 +119,21 @@ $(() => {
gl.IssueBoardsSearch = new Vue({
el: document.getElementById('js-add-list'),
data: {
filters: Store.state.filters
filters: Store.state.filters,
},
mounted () {
gl.issueBoards.newListDropdownInit();
}
},
});
gl.IssueBoardsModalAddBtn = new Vue({
mixins: [gl.issueBoards.ModalMixins],
el: document.getElementById('js-add-issues-btn'),
data: {
modal: ModalStore.store,
store: Store.state,
data() {
return {
modal: ModalStore.store,
store: Store.state,
};
},
watch: {
disabled() {
......@@ -133,6 +142,9 @@ $(() => {
},
computed: {
disabled() {
if (!this.store) {
return true;
}
return !this.store.lists.filter(list => !list.preset).length;
},
tooltipTitle() {
......@@ -145,7 +157,7 @@ $(() => {
},
methods: {
updateTooltip() {
const $tooltip = $(this.$el);
const $tooltip = $(this.$refs.addIssuesButton);
this.$nextTick(() => {
if (this.disabled) {
......@@ -165,16 +177,19 @@ $(() => {
this.updateTooltip();
},
template: `
<button
class="btn btn-create pull-right prepend-left-10"
type="button"
data-placement="bottom"
:class="{ 'disabled': disabled }"
:title="tooltipTitle"
:aria-disabled="disabled"
@click="openModal">
Add issues
</button>
<div class="board-extra-actions">
<button
class="btn btn-create prepend-left-10"
type="button"
data-placement="bottom"
ref="addIssuesButton"
:class="{ 'disabled': disabled }"
:title="tooltipTitle"
:aria-disabled="disabled"
@click="openModal">
Add issues
</button>
</div>
`,
});
});
......@@ -77,7 +77,7 @@ export default {
this.showIssueForm = !this.showIssueForm;
},
onScroll() {
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
if (!this.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) {
this.loadNextPage();
}
},
......@@ -165,11 +165,9 @@ export default {
v-if="loading">
<loading-icon />
</div>
<transition name="slide-down">
<board-new-issue
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
</transition>
<board-new-issue
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
<ul
class="board-list"
v-show="!loading"
......
......@@ -6,7 +6,10 @@ const Store = gl.issueBoards.BoardsStore;
export default {
name: 'BoardNewIssue',
props: {
list: Object,
list: {
type: Object,
required: true,
},
},
data() {
return {
......
......@@ -64,10 +64,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({
return this.issue.assignees.length > this.numberOverLimit;
},
cardUrl() {
return `${this.issueLinkBase}/${this.issue.id}`;
return `${this.issueLinkBase}/${this.issue.iid}`;
},
issueId() {
return `#${this.issue.id}`;
if (this.issue.iid) {
return `#${this.issue.iid}`;
}
return false;
},
showLabelFooter() {
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
......@@ -143,7 +146,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({
:title="issue.title">{{ issue.title }}</a>
<span
class="card-number"
v-if="issue.id"
v-if="issueId"
>
{{ issueId }}
</span>
......
......@@ -29,7 +29,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
const firstListIndex = 1;
const list = this.modal.selectedList || this.state.lists[firstListIndex];
const selectedIssues = ModalStore.getSelectedIssues();
const issueIds = selectedIssues.map(issue => issue.globalId);
const issueIds = selectedIssues.map(issue => issue.id);
// Post the data to the backend
gl.boardService.bulkUpdate(issueIds, {
......
......@@ -27,7 +27,7 @@ gl.issueBoards.newListDropdownInit = () => {
$this.glDropdown({
data(term, callback) {
$.get($this.attr('data-labels'))
$.get($this.attr('data-list-labels-path'))
.then((resp) => {
callback(resp);
});
......
......@@ -18,17 +18,33 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
type: Object,
required: true,
},
issueUpdate: {
type: String,
required: true,
},
},
computed: {
updateUrl() {
return this.issueUpdate;
},
},
methods: {
removeIssue() {
const issue = this.issue;
const lists = issue.getLists();
const labelIds = lists.map(list => list.label.id);
// Post the remove data
gl.boardService.bulkUpdate([issue.globalId], {
remove_label_ids: labelIds,
}).catch(() => {
const listLabelIds = lists.map(list => list.label.id);
let labelIds = this.issue.labels
.map(label => label.id)
.filter(id => !listLabelIds.includes(id));
if (labelIds.length === 0) {
labelIds = [''];
}
const data = {
issue: {
label_ids: labelIds,
},
};
Vue.http.patch(this.updateUrl, data).catch(() => {
new Flash('Failed to remove issue from board, please try again.', 'alert');
lists.forEach((list) => {
......
......@@ -7,8 +7,8 @@ import Vue from 'vue';
class ListIssue {
constructor (obj, defaultAvatar) {
this.globalId = obj.id;
this.id = obj.iid;
this.id = obj.id;
this.iid = obj.iid;
this.title = obj.title;
this.confidential = obj.confidential;
this.dueDate = obj.due_date;
......
......@@ -4,6 +4,7 @@ class ListLabel {
constructor (obj) {
this.id = obj.id;
this.title = obj.title;
this.type = obj.type;
this.color = obj.color;
this.textColor = obj.text_color;
this.description = obj.description;
......
......@@ -110,11 +110,13 @@ class List {
return gl.boardService.newIssue(this.id, issue)
.then(resp => resp.json())
.then((data) => {
issue.id = data.iid;
issue.id = data.id;
issue.iid = data.iid;
issue.project = data.project;
if (this.issuesSize > 1) {
const moveBeforeIid = this.issues[1].id;
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid);
const moveBeforeId = this.issues[1].id;
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeId);
}
});
}
......@@ -126,19 +128,19 @@ class List {
}
addIssue (issue, listFrom, newIndex) {
let moveBeforeIid = null;
let moveAfterIid = null;
let moveBeforeId = null;
let moveAfterId = null;
if (!this.findIssue(issue.id)) {
if (newIndex !== undefined) {
this.issues.splice(newIndex, 0, issue);
if (this.issues[newIndex - 1]) {
moveBeforeIid = this.issues[newIndex - 1].id;
moveBeforeId = this.issues[newIndex - 1].id;
}
if (this.issues[newIndex + 1]) {
moveAfterIid = this.issues[newIndex + 1].id;
moveAfterId = this.issues[newIndex + 1].id;
}
} else {
this.issues.push(issue);
......@@ -151,30 +153,30 @@ class List {
if (listFrom) {
this.issuesSize += 1;
this.updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid);
this.updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId);
}
}
}
moveIssue (issue, oldIndex, newIndex, moveBeforeIid, moveAfterIid) {
moveIssue (issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
this.issues.splice(oldIndex, 1);
this.issues.splice(newIndex, 0, issue);
gl.boardService.moveIssue(issue.id, null, null, moveBeforeIid, moveAfterIid)
gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId)
.catch(() => {
// TODO: handle request error
});
}
updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) {
gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid)
updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) {
gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId)
.catch(() => {
// TODO: handle request error
});
}
findIssue (id) {
return this.issues.filter(issue => issue.id === id)[0];
return this.issues.find(issue => issue.id === id);
}
removeIssue (removeIssue) {
......
......@@ -3,21 +3,21 @@
import Vue from 'vue';
class BoardService {
constructor (root, bulkUpdatePath, boardId) {
this.boards = Vue.resource(`${root}{/id}.json`, {}, {
constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
issues: {
method: 'GET',
url: `${root}/${boardId}/issues.json`
url: `${gon.relative_url_root}/boards/${boardId}/issues.json`,
}
});
this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
this.lists = Vue.resource(`${listsEndpoint}{/id}`, {}, {
generate: {
method: 'POST',
url: `${root}/${boardId}/lists/generate.json`
url: `${listsEndpoint}/generate.json`
}
});
this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {});
this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}, {
this.issue = Vue.resource(`${gon.relative_url_root}/boards/${boardId}/issues{/id}`, {});
this.issues = Vue.resource(`${listsEndpoint}{/id}/issues`, {}, {
bulkUpdate: {
method: 'POST',
url: bulkUpdatePath,
......@@ -60,12 +60,12 @@ class BoardService {
return this.issues.get(data);
}
moveIssue (id, from_list_id = null, to_list_id = null, move_before_iid = null, move_after_iid = null) {
moveIssue (id, from_list_id = null, to_list_id = null, move_before_id = null, move_after_id = null) {
return this.issue.update({ id }, {
from_list_id,
to_list_id,
move_before_iid,
move_after_iid,
move_before_id,
move_after_id,
});
}
......
......@@ -21,8 +21,10 @@ let headerHeight = 50;
export const getHeaderHeight = () => headerHeight;
export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-icons-only');
export const canShowActiveSubItems = (el) => {
if (el.classList.contains('active') && (sidebar && !sidebar.classList.contains('sidebar-icons-only'))) {
if (el.classList.contains('active') && !isSidebarCollapsed()) {
return false;
}
......@@ -100,12 +102,13 @@ export const moveSubItemsToPosition = (el, subItems) => {
export const showSubLevelItems = (el) => {
const subItems = el.querySelector('.sidebar-sub-level-items');
const isIconOnly = subItems && subItems.classList.contains('is-fly-out-only');
if (!canShowSubItems() || !canShowActiveSubItems(el)) return;
el.classList.add(IS_OVER_CLASS);
if (!subItems) return;
if (!subItems || (!isSidebarCollapsed() && isIconOnly)) return;
subItems.style.display = 'block';
el.classList.add(IS_SHOWING_FLY_OUT_CLASS);
......
......@@ -73,7 +73,7 @@ class Issue {
$(document).trigger('issuable:change', isClosed);
this.toggleCloseReopenButton(isClosed);
let numProjectIssues = Number(projectIssuesCounter.text().replace(/[^\d]/, ''));
let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues));
......
......@@ -19,6 +19,7 @@
@import "framework/flash";
@import "framework/forms";
@import "framework/gfm";
@import "framework/gitlab-theme";
@import "framework/header";
@import "framework/highlight";
@import "framework/issue_box";
......
......@@ -412,11 +412,12 @@ table {
.gl-accessibility {
&:focus {
display: flex;
align-items: center;
top: 1px;
left: 1px;
width: auto;
height: 100%;
line-height: 50px;
padding: 0 10px;
clip: auto;
text-decoration: none;
......
......@@ -183,7 +183,7 @@
width: auto;
top: 100%;
left: 0;
z-index: 200;
z-index: 300;
min-width: 240px;
max-width: 500px;
margin-top: 2px;
......@@ -837,17 +837,30 @@
}
}
@media (max-width: $screen-xs-max) {
.navbar-gitlab {
li.header-projects,
li.header-more,