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

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

parents 2c4d57ff 12ddc28f
......@@ -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)
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: {
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: `
<div class="board-extra-actions">
<button
class="btn btn-create pull-right prepend-left-10"
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>
<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,
li.header-new,
li.header-user {
position: static;
}
}
header.navbar-gitlab .dropdown {
.dropdown-menu,
.dropdown-menu-nav {
width: 100%;
min-width: 100%;
}
}
}
@include new-style-dropdown('.breadcrumbs-list .dropdown ');
@include new-style-dropdown('.js-namespace-select + ');
header.navbar-gitlab-new .header-content .dropdown-menu.projects-dropdown-menu {
padding: 0;
@media (max-width: $screen-xs-max) {
display: table;
left: -50px;
min-width: 300px;
}
}
.projects-dropdown-container {
......
/**
* Styles the GitLab application with a specific color theme
*/
@mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) {
// Header
header.navbar-gitlab-new {
background: linear-gradient(to right, $color-900, $color-800);
.navbar-collapse {
color: $color-200;
}
.container-fluid {
.navbar-toggle {
border-left: 1px solid lighten($color-700, 10%);
}
}
.navbar-sub-nav,
.navbar-nav {
> li {
> a:hover,
> a:focus {
background-color: rgba($color-200, .2);
}
&.active > a,
&.dropdown.open > a {
color: $color-900;
background-color: $color-alternate;
svg {
fill: currentColor;
}
}
&.line-separator {
border-left: 1px solid rgba($color-200, .2);
}
}
}
.navbar-sub-nav {
color: $color-200;
}
.nav {
> li {
color: $color-200;
> a {
svg {
fill: $color-200;
}
&.header-user-dropdown-toggle {
.header-user-avatar {
border-color: $color-200;
}
}
&:hover,
&:focus {
@media (min-width: $screen-sm-min) {
background-color: rgba($color-200, .2);
}
svg {
fill: currentColor;
}
}
}
&.active > a,
&.dropdown.open > a {
color: $color-900;
background-color: $color-alternate;
&:hover {
svg {
fill: $color-900;
}
}
}
.impersonated-user,
.impersonated-user:hover {
svg {
fill: $color-900;
}
}
}
}
}
.title {
> a {
&:hover,
&:focus {
background-color: rgba($color-200, .2);
}
}
}
.search {
form {
background-color: rgba($color-200, .2);
&:hover {
background-color: rgba($color-200, .3);
}
}
.location-badge {
color: $color-100;
background-color: rgba($color-200, .1);
border-right: 1px solid $color-800;
}
.search-input::placeholder {
color: rgba($color-200, .8);
}
.search-input-wrap {
.search-icon,
.clear-icon {
color: rgba($color-200, .8);
}
}
&.search-active {
form {
background-color: $white-light;
}
.location-badge {
color: $gl-text-color;
}
.search-input-wrap {
.search-icon {
color: rgba($color-200, .8);
}
}
}
}
.btn-sign-in {
background-color: $color-100;
color: $color-900;
}
// Sidebar
.nav-sidebar li.active {
box-shadow: inset 4px 0 0 $color-700;
> a {
color: $color-900;
}
svg {
fill: $color-900;
}
}
}
body {
&.ui_indigo {
@include gitlab-theme($indigo-100, $indigo-200, $indigo-500, $indigo-700, $indigo-800, $indigo-900, $white-light);
}
&.ui_dark {
@include gitlab-theme($theme-gray-100, $theme-gray-200, $theme-gray-500, $theme-gray-700, $theme-gray-800, $theme-gray-900, $white-light);
}
&.ui_blue {
@include gitlab-theme($theme-blue-100, $theme-blue-200, $theme-blue-500, $theme-blue-700, $theme-blue-800, $theme-blue-900, $white-light);
}
&.ui_green {
@include gitlab-theme($theme-green-100, $theme-green-200, $theme-green-500, $theme-green-700, $theme-green-800, $theme-green-900, $white-light);
}
&.ui_light {
@include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700);
header.navbar-gitlab-new {
background: $theme-gray-100;
box-shadow: 0 2px 0 0 $border-color;
.logo-text svg {
fill: $theme-gray-900;
}
.navbar-sub-nav,
.navbar-nav {
> li {
> a:hover,
> a:focus {
color: $theme-gray-900;
}
&.active > a {
color: $white-light;
&:hover {
color: $white-light;
}
}
}
}
.container-fluid {
.navbar-toggle,
.navbar-toggle:hover {
color: $theme-gray-700;
border-left: 1px solid $theme-gray-200;
}
}
}
.search {
form {
background-color: $white-light;
box-shadow: inset 0 0 0 1px $border-color;
&:hover {
background-color: $white-light;
box-shadow: inset 0 0 0 1px $blue-100;
.location-badge {
box-shadow: inset 0 0 0 1px $blue-100;
}
}
}
.search-input-wrap {
.search-icon {
color: $theme-gray-200;
}
}
.location-badge {
color: $theme-gray-700;
box-shadow: inset 0 0 0 1px $border-color;
background-color: $nav-badge-bg;
border-right: 0;
}
}
.nav-sidebar li.active {
> a {
color: $theme-gray-900;
}
svg {
fill: $theme-gray-900;
}
}
}
}
......@@ -111,7 +111,6 @@ header {
svg {
height: 16px;
width: 23px;
fill: currentColor;
}
}
......
......@@ -74,6 +74,8 @@ $red-700: #a62d19;
$red-800: #8b2615;
$red-900: #711e11;
// GitLab themes
$indigo-50: #f7f7ff;
$indigo-100: #ebebfa;
$indigo-200: #d1d1f0;
......@@ -86,6 +88,43 @@ $indigo-800: #393982;
$indigo-900: #292961;
$indigo-950: #1a1a40;
$theme-gray-50: #fafafa;
$theme-gray-100: #f2f2f2;
$theme-gray-200: #dfdfdf;
$theme-gray-300: #cccccc;
$theme-gray-400: #bababa;
$theme-gray-500: #a7a7a7;
$theme-gray-600: #949494;
$theme-gray-700: #707070;
$theme-gray-800: #4f4f4f;
$theme-gray-900: #2e2e2e;
$theme-gray-950: #1f1f1f;
$theme-blue-50: #f4f8fc;
$theme-blue-100: #e6edf5;
$theme-blue-200: #c8d7e6;
$theme-blue-300: #97b3cf;
$theme-blue-400: #648cb4;
$theme-blue-500: #4a79a8;
$theme-blue-600: #3e6fa0;
$theme-blue-700: #305c88;
$theme-blue-800: #25496e;
$theme-blue-900: #1a3652;
$theme-blue-950: #0f2235;
$theme-green-50: #f2faf6;
$theme-green-100: #e4f3ea;
$theme-green-200: #c0dfcd;
$theme-green-300: #8ac2a1;
$theme-green-400: #52a274;
$theme-green-500: #35935c;
$theme-green-600: #288a50;
$theme-green-700: #1c7441;
$theme-green-800: #145d33;
$theme-green-900: #0d4524;
$theme-green-950: #072d16;
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
$almost-black: #242424;
......
......@@ -9,10 +9,20 @@
header.navbar-gitlab-new {
color: $white-light;
background: linear-gradient(to right, $indigo-900, $indigo-800);
border-bottom: 0;
min-height: $new-navbar-height;
.logo-text {
line-height: initial;
svg {
width: 55px;
height: 14px;
margin: 0;
fill: $white-light;
}
}
.header-content {
display: -webkit-flex;
display: flex;
......@@ -38,10 +48,10 @@ header.navbar-gitlab-new {
img {
height: 28px;
margin-right: 10px;
margin-right: 8px;
}
> a {
a {
display: -webkit-flex;
display: flex;
align-items: center;
......@@ -54,22 +64,6 @@ header.navbar-gitlab-new {
margin-right: 8px;
}
}
.logo-text {
line-height: initial;
svg {
width: 55px;
height: 14px;
margin: 0;
fill: $white-light;
}
}
&:hover,
&:focus {
background-color: rgba($indigo-200, .2);
}
}
}
......@@ -106,7 +100,6 @@ header.navbar-gitlab-new {
.navbar-collapse {
padding-left: 0;
color: $indigo-200;
box-shadow: 0;
@media (max-width: $screen-xs-max) {
......@@ -132,7 +125,6 @@ header.navbar-gitlab-new {
font-size: 14px;
text-align: center;
color: currentColor;
border-left: 1px solid lighten($indigo-700, 10%);
&:hover,
&:focus,
......@@ -167,42 +159,27 @@ header.navbar-gitlab-new {
will-change: color;
margin: 4px 2px;
padding: 6px 8px;
color: $indigo-200;
height: 32px;
@media (max-width: $screen-xs-max) {
padding: 0;
}
svg {
fill: $indigo-200;
}
&.header-user-dropdown-toggle {
margin-left: 2px;
.header-user-avatar {
border-color: $indigo-200;
margin-right: 0;
}
}
}
.header-new-dropdown-toggle {
margin-right: 0;
}
> a:hover,
> a:focus {
&:hover,
&:focus {
text-decoration: none;
outline: 0;
opacity: 1;
color: $white-light;
@media (min-width: $screen-sm-min) {
background-color: rgba($indigo-200, .2);
}
svg {
fill: currentColor;
}
......@@ -213,6 +190,11 @@ header.navbar-gitlab-new {
}
}
}
}
.header-new-dropdown-toggle {
margin-right: 0;
}
.impersonated-user,
.impersonated-user:hover {
......@@ -220,10 +202,6 @@ header.navbar-gitlab-new {
background-color: $white-light;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
svg {
fill: $indigo-900;
}
}
.impersonation-btn,
......@@ -241,8 +219,6 @@ header.navbar-gitlab-new {
&.active > a,
&.dropdown.open > a {
color: $indigo-900;
background-color: $white-light;
svg {
fill: currentColor;
......@@ -256,7 +232,6 @@ header.navbar-gitlab-new {
display: -webkit-flex;
display: flex;
margin: 0 0 0 6px;
color: $indigo-200;
.dropdown-chevron {
position: relative;
......@@ -274,17 +249,6 @@ header.navbar-gitlab-new {
text-decoration: none;
outline: 0;
color: $white-light;
background-color: rgba($indigo-200, .2);
svg {
fill: currentColor;
}
}
&.active > a,
&.dropdown.open > a {
color: $indigo-900;
background-color: $white-light;
svg {
fill: currentColor;
......@@ -309,7 +273,6 @@ header.navbar-gitlab-new {
}
&.line-separator {
border-left: 1px solid rgba($indigo-200, .2);
margin: 8px;
}
}
......@@ -339,17 +302,14 @@ header.navbar-gitlab-new {
height: 32px;
border: 0;
border-radius: $border-radius-default;
background-color: rgba($indigo-200, .2);
transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s;
&:hover {
background-color: rgba($indigo-200, .3);
box-shadow: none;
}
}
&.search-active form {
background-color: $white-light;
box-shadow: none;
.search-input {
......@@ -377,43 +337,26 @@ header.navbar-gitlab-new {
}
.search-input::placeholder {
color: rgba($indigo-200, .8);
transition: color ease-in-out 0.15s;
}
.location-badge {
font-size: 12px;
color: $indigo-100;
background-color: rgba($indigo-200, .1);
will-change: color;
margin: -4px 4px -4px -4px;
line-height: 25px;
padding: 4px 8px;
border-radius: 2px 0 0 2px;
border-right: 1px solid $indigo-800;
height: 32px;
transition: border-color ease-in-out 0.15s;
}
.search-input-wrap {
.search-icon,
.clear-icon {
color: rgba($indigo-200, .8);
}
}
&.search-active {
.location-badge {
color: $gl-text-color;
background-color: $nav-badge-bg;
border-color: $border-color;
}
.search-input-wrap {
.search-icon {
color: rgba($indigo-200, .8);
}
.clear-icon {
color: $white-light;
}
......@@ -517,8 +460,6 @@ header.navbar-gitlab-new {
.btn-sign-in {
margin-top: 3px;
background-color: $indigo-100;
color: $indigo-900;
font-weight: $gl-font-weight-bold;
&:hover {
......
......@@ -106,11 +106,8 @@ $new-sidebar-collapsed-width: 50px;
overflow-x: hidden;
}
.badge,
.sidebar-context-title {
display: none;
}
.badge:not(.fly-out-badge),
.sidebar-context-title,
.nav-item-name {
display: none;
}
......@@ -118,6 +115,10 @@ $new-sidebar-collapsed-width: 50px;
.sidebar-top-level-items > li > a {
min-height: 44px;
}
.fly-out-top-item {
display: block;
}
}
&.nav-sidebar-expanded {
......@@ -154,16 +155,9 @@ $new-sidebar-collapsed-width: 50px;
}
li.active {
box-shadow: inset 4px 0 0 $active-border;
> a {
color: $active-color;
font-weight: $gl-font-weight-bold;
}
svg {
fill: $active-color;
}
}
@media (max-width: $screen-xs-max) {
......@@ -179,6 +173,10 @@ $new-sidebar-collapsed-width: 50px;
width: 16px;
}
}
.fly-out-top-item {
display: none;
}
}
.nav-sidebar-inner-scroll {
......@@ -249,7 +247,7 @@ $new-sidebar-collapsed-width: 50px;
left: $new-sidebar-width;
min-width: 150px;
margin-top: -1px;
padding: 8px 1px;
padding: 4px 1px;
background-color: $white-light;
box-shadow: 2px 1px 3px $dropdown-shadow-color;
border: 1px solid $gray-darker;
......@@ -270,6 +268,13 @@ $new-sidebar-collapsed-width: 50px;
margin-top: 1px;
}
.divider {
height: 1px;
margin: 4px -1px;
padding: 0;
background-color: $dropdown-divider-color;
}
> .active {
box-shadow: none;
......@@ -309,7 +314,7 @@ $new-sidebar-collapsed-width: 50px;
font-weight: $gl-font-weight-bold;
}
.sidebar-sub-level-items {
.sidebar-sub-level-items:not(.is-fly-out-only) {
display: block;
}
}
......@@ -407,6 +412,19 @@ $new-sidebar-collapsed-width: 50px;
}
}
.fly-out-top-item {
> a {
display: flex;
}
.fly-out-badge {
margin-left: 8px;
}
}
.fly-out-top-item-name {
flex: 1;
}
// Mobile nav
......
......@@ -117,13 +117,12 @@
}
.board-title {
position: initial;
padding: 0;
border-bottom: 0;
> span {
display: block;
transform: rotate(90deg) translate(25px, 0);
transform: rotate(90deg) translate(35px, 10px);
}
}
......@@ -151,11 +150,18 @@
}
.board-header {
border-top-left-radius: $border-radius-default;
border-top-right-radius: $border-radius-default;
position: relative;
&.has-border {
&.has-border::before {
border-top: 3px solid;
border-color: inherit;
border-top-left-radius: $border-radius-default;
border-top-right-radius: $border-radius-default;
content: '';
position: absolute;
width: calc(100% + 2px);
top: 0;
left: 0;
margin-top: -1px;
margin-right: -1px;
margin-left: -1px;
......@@ -176,12 +182,16 @@
}
.board-title {
position: relative;
margin: 0;
padding: $gl-padding;
padding-bottom: ($gl-padding + 3px);
padding: 12px $gl-padding;
font-size: 1em;
border-bottom: 1px solid $border-color;
display: flex;
align-items: center;
}
.board-title-text {
margin-right: auto;
}
.board-delete {
......@@ -221,43 +231,10 @@
}
}
.slide-down-enter {
transform: translateY(-100%);
}
.slide-down-enter-active {
transition: transform $fade-in-duration;
+ .board-list {
transform: translateY(-136px);
transition: none;
}
}
.slide-down-enter-to {
+ .board-list {
transform: translateY(0);
transition: transform $fade-in-duration ease;
}
}
.slide-down-leave {
transform: translateY(0);
}
.slide-down-leave-active {
transition: all $fade-in-duration;
transform: translateY(-136px);
+ .board-list {
transition: transform $fade-in-duration ease;
transform: translateY(-136px);
}
}
.board-list-component {
height: calc(100% - 49px);
overflow: hidden;
position: relative;
}
.board-list {
......@@ -429,7 +406,7 @@
}
.board-new-issue-form {
z-index: 1;
z-index: 4;
margin: 5px;
}
......
@mixin application-theme-preview($color-1, $color-2, $color-3, $color-4) {
.one {
background-color: $color-1;
border-top-left-radius: $border-radius-default;
}
.two {
background-color: $color-2;
border-top-right-radius: $border-radius-default;
}
.three {
background-color: $color-3;
border-bottom-left-radius: $border-radius-default;
}
.four {
background-color: $color-4;
border-bottom-right-radius: $border-radius-default;
}
}
.application-theme {
label {
margin-right: 20px;
text-align: center;
}
.preview {
font-size: 0;
margin-bottom: 10px;
&.indigo {
@include application-theme-preview($indigo-900, $indigo-700, $indigo-800, $indigo-500);
}
&.dark {
@include application-theme-preview($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-600);
}
&.light {
@include application-theme-preview($theme-gray-600, $theme-gray-200, $theme-gray-400, $theme-gray-100);
}
&.blue {
@include application-theme-preview($theme-blue-900, $theme-blue-700, $theme-blue-800, $theme-blue-500);
}
&.green {
@include application-theme-preview($theme-green-900, $theme-green-700, $theme-green-800, $theme-green-500);
}
}
.preview-row {
display: block;
}
.quadrant {
display: inline-block;
height: 50px;
width: 80px;
}
}
.syntax-theme {
label {
margin-right: 20px;
......
......@@ -166,7 +166,7 @@ input[type="checkbox"]:hover {
.dropdown-menu {
transition-duration: 100ms, 75ms;
transition-delay: 75ms, 100ms;
transform: translateY(13px);
transform: translateY(7px);
opacity: 1;
}
}
......
......@@ -211,6 +211,7 @@ class Admin::UsersController < Admin::ApplicationController
:provider,
:remember_me,
:skype,
:theme_id,
:twitter,
:username,
:website_url
......
module Boards
class ApplicationController < ::ApplicationController
respond_to :json
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
private
def board
@board ||= Board.find(params[:board_id])
end
def board_parent
@board_parent ||= board.parent
end
def record_not_found(exception)
render json: { error: exception.message }, status: :not_found
end
end
end
module Boards
class IssuesController < Boards::ApplicationController
include BoardsResponses
before_action :authorize_read_issue, only: [:index]
before_action :authorize_create_issue, only: [:create]
before_action :authorize_update_issue, only: [:update]
skip_before_action :authenticate_user!, only: [:index]
def index
issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute
issues = issues.page(params[:page]).per(params[:per] || 20)
make_sure_position_is_set(issues)
render json: {
issues: serialize_as_json(issues.preload(:project)),
size: issues.total_count
}
end
def create
service = Boards::Issues::CreateService.new(board_parent, project, current_user, issue_params)
issue = service.execute
if issue.valid?
render json: serialize_as_json(issue)
else
render json: issue.errors, status: :unprocessable_entity
end
end
def update
service = Boards::Issues::MoveService.new(board_parent, current_user, move_params)
if service.execute(issue)
head :ok
else
head :unprocessable_entity
end
end
private
def make_sure_position_is_set(issues)
issues.each do |issue|
issue.move_to_end && issue.save unless issue.relative_position
end
end
def issue
@issue ||= issues_finder.execute.find(params[:id])
end
def filter_params
params.merge(board_id: params[:board_id], id: params[:list_id])
.reject { |_, value| value.nil? }
end
def issues_finder
IssuesFinder.new(current_user, project_id: board_parent.id)
end
def project
board_parent
end
def move_params
params.permit(:board_id, :id, :from_list_id, :to_list_id, :move_before_id, :move_after_id)
end
def issue_params
params.require(:issue)
.permit(:title, :milestone_id, :project_id)
.merge(board_id: params[:board_id], list_id: params[:list_id], request: request)
end
def serialize_as_json(resource)
resource.as_json(
labels: true,
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
include: {
project: { only: [:id, :path] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
milestone: { only: [:id, :title] }
},
user: current_user
)
end
end
end
module Boards
class ListsController < Boards::ApplicationController
include BoardsResponses
before_action :authorize_admin_list, only: [:create, :update, :destroy, :generate]
before_action :authorize_read_list, only: [:index]
skip_before_action :authenticate_user!, only: [:index]
def index
lists = Boards::Lists::ListService.new(board.parent, current_user).execute(board)
render json: serialize_as_json(lists)
end
def create
list = Boards::Lists::CreateService.new(board.parent, current_user, list_params).execute(board)
if list.valid?
render json: serialize_as_json(list)
else
render json: list.errors, status: :unprocessable_entity
end
end
def update
list = board.lists.movable.find(params[:id])
service = Boards::Lists::MoveService.new(board_parent, current_user, move_params)
if service.execute(list)
head :ok
else
head :unprocessable_entity
end
end
def destroy
list = board.lists.destroyable.find(params[:id])
service = Boards::Lists::DestroyService.new(board_parent, current_user)
if service.execute(list)
head :ok
else
head :unprocessable_entity
end
end
def generate
service = Boards::Lists::GenerateService.new(board_parent, current_user)
if service.execute(board)
render json: serialize_as_json(board.lists.movable)
else
head :unprocessable_entity
end
end
private
def list_params
params.require(:list).permit(:label_id)
end
def move_params
params.require(:list).permit(:position)
end
def serialize_as_json(resource)
resource.as_json(
only: [:id, :list_type, :position],
methods: [:title],
label: true
)
end
end
end
module BoardsResponses
def authorize_read_list
authorize_action_for!(board.parent, :read_list)
end
def authorize_read_issue
authorize_action_for!(board.parent, :read_issue)
end
def authorize_update_issue
authorize_action_for!(issue, :admin_issue)
end
def authorize_create_issue
authorize_action_for!(project, :admin_issue)
end
def authorize_admin_list
authorize_action_for!(board.parent, :admin_list)
end
def authorize_action_for!(resource, ability)
return render_403 unless can?(current_user, ability, resource)
end
def respond_with_boards
respond_with(@boards)
end
def respond_with_board
respond_with(@board)
end
def respond_with(resource)
respond_to do |format|
format.html
format.json do
render json: serialize_as_json(resource)
end
end
end
end
......@@ -35,7 +35,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:color_scheme_id,
:layout,
:dashboard,
:project_view
:project_view,
:theme_id
)
end
end
module Projects
module Boards
class ApplicationController < Projects::ApplicationController
respond_to :json
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
private
def record_not_found(exception)
render json: { error: exception.message }, status: :not_found
end
end
end
end
module Projects
module Boards
class IssuesController < Boards::ApplicationController
before_action :authorize_read_issue!, only: [:index]
before_action :authorize_create_issue!, only: [:create]
before_action :authorize_update_issue!, only: [:update]
def index
issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
issues = issues.page(params[:page]).per(params[:per] || 20)
make_sure_position_is_set(issues)
render json: {
issues: serialize_as_json(issues),
size: issues.total_count
}
end
def create
service = ::Boards::Issues::CreateService.new(project, current_user, issue_params)
issue = service.execute
if issue.valid?
render json: serialize_as_json(issue)
else
render json: issue.errors, status: :unprocessable_entity
end
end
def update
service = ::Boards::Issues::MoveService.new(project, current_user, move_params)
if service.execute(issue)
head :ok
else
head :unprocessable_entity
end
end
private
def make_sure_position_is_set(issues)
issues.each do |issue|
issue.move_to_end && issue.save unless issue.relative_position
end
end
def issue
@issue ||=
IssuesFinder.new(current_user, project_id: project.id)
.execute
.where(iid: params[:id])
.first!
end
def authorize_read_issue!
return render_403 unless can?(current_user, :read_issue, project)
end
def authorize_create_issue!
return render_403 unless can?(current_user, :admin_issue, project)
end
def authorize_update_issue!
return render_403 unless can?(current_user, :update_issue, issue)
end
def filter_params
params.merge(board_id: params[:board_id], id: params[:list_id])
.reject { |_, value| value.nil? }
end
def move_params
params.permit(:board_id, :id, :from_list_id, :to_list_id, :move_before_iid, :move_after_iid)
end
def issue_params
params.require(:issue).permit(:title).merge(board_id: params[:board_id], list_id: params[:list_id], request: request)
end
def serialize_as_json(resource)
resource.as_json(
labels: true,
only: [:id, :iid, :title, :confidential, :due_date, :relative_position],
include: {
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
milestone: { only: [:id, :title] }
},
user: current_user
)
end
end
end
end
module Projects
module Boards
class ListsController < Boards::ApplicationController
before_action :authorize_admin_list!, only: [:create, :update, :destroy, :generate]
before_action :authorize_read_list!, only: [:index]
def index
lists = ::Boards::Lists::ListService.new(project, current_user).execute(board)
render json: serialize_as_json(lists)
end
def create
list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute(board)
if list.valid?
render json: serialize_as_json(list)
else
render json: list.errors, status: :unprocessable_entity
end
end
def update
list = board.lists.movable.find(params[:id])
service = ::Boards::Lists::MoveService.new(project, current_user, move_params)
if service.execute(list)
head :ok
else
head :unprocessable_entity
end
end
def destroy
list = board.lists.destroyable.find(params[:id])
service = ::Boards::Lists::DestroyService.new(project, current_user)
if service.execute(list)
head :ok
else
head :unprocessable_entity
end
end
def generate
service = ::Boards::Lists::GenerateService.new(project, current_user)
if service.execute(board)
render json: serialize_as_json(board.lists.movable)
else
head :unprocessable_entity
end
end
private
def authorize_admin_list!
return render_403 unless can?(current_user, :admin_list, project)
end
def authorize_read_list!
return render_403 unless can?(current_user, :read_list, project)
end
def board
@board ||= project.boards.find(params[:board_id])
end
def list_params
params.require(:list).permit(:label_id)
end
def move_params
params.require(:list).permit(:position)
end
def serialize_as_json(resource)
resource.as_json(
only: [:id, :list_type, :position],
methods: [:title],
label: true
)
end
end
end
end
class Projects::BoardsController < Projects::ApplicationController
include BoardsResponses
include IssuableCollections
before_action :authorize_read_board!, only: [:index, :show]
before_action :assign_endpoint_vars
def index
@boards = ::Boards::ListService.new(project, current_user).execute
@boards = Boards::ListService.new(project, current_user).execute
respond_to do |format|
format.html
format.json do
render json: serialize_as_json(@boards)
end
end
respond_with_boards
end
def show
@board = project.boards.find(params[:id])
respond_to do |format|
format.html
format.json do
render json: serialize_as_json(@board)
end
end
respond_with_board
end
private
def assign_endpoint_vars
@boards_endpoint = project_boards_url(project)
@bulk_issues_path = bulk_update_project_issues_path(project)
@namespace_path = project.namespace.full_path
@labels_endpoint = project_labels_path(project)
end
def authorize_read_board!
return access_denied! unless can?(current_user, :read_board, project)
end
......
module BoardsHelper
def board_data
board = @board || @boards.first
def board
@board ||= @board || @boards.first
end
def board_data
{
endpoint: project_boards_path(@project),
boards_endpoint: @boards_endpoint,
lists_endpoint: board_lists_url(board),
board_id: board.id,
disabled: "#{!can?(current_user, :admin_list, @project)}",
issue_link_base: project_issues_path(@project),
disabled: "#{!can?(current_user, :admin_list, current_board_parent)}",
issue_link_base: build_issue_link_base,
root_path: root_path,
bulk_update_path: bulk_update_project_issues_path(@project),
bulk_update_path: @bulk_issues_path,
default_avatar: image_path(default_avatar)
}
end
def build_issue_link_base
project_issues_path(@project)
end
def current_board_json
board = @board || @boards.first
board.to_json(
only: [:id, :name, :milestone_id],
include: {
milestone: { only: [:title] }
}
)
end
def board_base_url
project_boards_path(@project)
end
def multiple_boards_available?
current_board_parent.multiple_issue_boards_available?(current_user)
end
def current_board_path(board)
@current_board_path ||= project_board_path(current_board_parent, board)
end
def current_board_parent
@current_board_parent ||= @project
end
def can_admin_issue?
can?(current_user, :admin_issue, current_board_parent)
end
def board_list_data
{
toggle: "dropdown",
list_labels_path: labels_filter_path(true),
labels: labels_filter_path(true),
labels_endpoint: @labels_endpoint,
namespace_path: @namespace_path,
project_path: @project&.try(:path)
}
end
def board_sidebar_user_data
dropdown_options = issue_assignees_dropdown_options
{
toggle: 'dropdown',
field_name: 'issue[assignee_ids][]',
first_user: current_user&.username,
current_user: 'true',
project_id: @project&.try(:id),
null_user: 'true',
multi_select: 'true',
'dropdown-header': dropdown_options[:data][:'dropdown-header'],
'max-select': dropdown_options[:data][:'max-select']
}
end
end
......@@ -347,6 +347,14 @@ module IssuablesHelper
end
end
def labels_path
if @project
project_labels_path(@project)
elsif @group
group_labels_path(@group)
end
end
def issuable_sidebar_options(issuable, can_edit_issuable)
{
endpoint: "#{issuable_json_path(issuable)}?basic=true",
......
......@@ -121,13 +121,14 @@ module LabelsHelper
end
end
def labels_filter_path
return group_labels_path(@group, :json) if @group
def labels_filter_path(only_group_labels = false)
project = @target_project || @project
if project
project_labels_path(project, :json)
elsif @group
options = { only_group_labels: only_group_labels } if only_group_labels
group_labels_path(@group, :json, options)
else
dashboard_labels_path(:json)
end
......
......@@ -40,6 +40,10 @@ module PreferencesHelper
]
end
def user_application_theme
@user_application_theme ||= Gitlab::Themes.for_user(current_user).css_class
end
def user_color_scheme
Gitlab::ColorSchemes.for_user(current_user).css_class
end
......
......@@ -134,7 +134,8 @@ module SearchHelper
end
def search_filter_input_options(type)
opts = {
opts =
{
id: "filtered-search-#{type}",
placeholder: 'Search or filter results...',
data: {
......@@ -147,6 +148,7 @@ module SearchHelper
opts[:data]['base-endpoint'] = project_path(@project)
else
# Group context
opts[:data]['group-id'] = @group.id
opts[:data]['base-endpoint'] = group_canonical_path(@group)
end
......
......@@ -3,7 +3,19 @@ class Board < ActiveRecord::Base
has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
validates :project, presence: true
validates :project, presence: true, if: :project_needed?
def project_needed?
true
end
def parent
project
end
def group_board?
false
end
def backlog_list
lists.merge(List.backlog).take
......
......@@ -10,8 +10,12 @@ module RelativePositioning
after_save :save_positionable_neighbours
end
def project_ids
[project.id]
end
def max_relative_position
self.class.in_projects(project.id).maximum(:relative_position)
self.class.in_projects(project_ids).maximum(:relative_position)
end
def prev_relative_position
......@@ -19,7 +23,7 @@ module RelativePositioning
if self.relative_position
prev_pos = self.class
.in_projects(project.id)
.in_projects(project_ids)
.where('relative_position < ?', self.relative_position)
.maximum(:relative_position)
end
......@@ -32,7 +36,7 @@ module RelativePositioning
if self.relative_position
next_pos = self.class
.in_projects(project.id)
.in_projects(project_ids)
.where('relative_position > ?', self.relative_position)
.minimum(:relative_position)
end
......@@ -59,7 +63,7 @@ module RelativePositioning
pos_after = before.next_relative_position
if before.shift_after?
issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_after)
issue_to_move = self.class.in_projects(project_ids).find_by!(relative_position: pos_after)
issue_to_move.move_after
@positionable_neighbours = [issue_to_move]
......@@ -74,7 +78,7 @@ module RelativePositioning
pos_before = after.prev_relative_position
if after.shift_before?
issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_before)
issue_to_move = self.class.in_projects(project_ids).find_by!(relative_position: pos_before)
issue_to_move.move_before
@positionable_neighbours = [issue_to_move]
......
class Event < ActiveRecord::Base
include Sortable
include IgnorableColumn
default_scope { reorder(nil).where.not(author_id: nil) }
CREATED = 1
......@@ -50,13 +51,9 @@ class Event < ActiveRecord::Base
belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
has_one :push_event_payload, foreign_key: :event_id
# For Hash only
serialize :data # rubocop:disable Cop/ActiveRecordSerialize
# Callbacks
after_create :reset_project_activity
after_create :set_last_repository_updated_at, if: :push?
after_create :replicate_event_for_push_events_migration
# Scopes
scope :recent, -> { reorder(id: :desc) }
......@@ -82,6 +79,10 @@ class Event < ActiveRecord::Base
self.inheritance_column = 'action'
# "data" will be removed in 10.0 but it may be possible that JOINs happen that
# include this column, hence we're ignoring it as well.
ignore_column :data
class << self
def model_name
ActiveModel::Name.new(self, nil, 'event')
......@@ -159,7 +160,7 @@ class Event < ActiveRecord::Base
end
def push?
action == PUSHED && valid_push?
false
end
def merged?
......@@ -272,87 +273,6 @@ class Event < ActiveRecord::Base
end
end
def valid_push?
data[:ref] && ref_name.present?
rescue
false
end
def tag?
Gitlab::Git.tag_ref?(data[:ref])
end
def branch?
Gitlab::Git.branch_ref?(data[:ref])
end
def new_ref?
Gitlab::Git.blank_ref?(commit_from)
end
def rm_ref?
Gitlab::Git.blank_ref?(commit_to)
end
def md_ref?
!(rm_ref? || new_ref?)
end
def commit_from
data[:before]
end
def commit_to
data[:after]
end
def ref_name
if tag?
tag_name
else
branch_name
end
end
def branch_name
@branch_name ||= Gitlab::Git.ref_name(data[:ref])
end
def tag_name
@tag_name ||= Gitlab::Git.ref_name(data[:ref])
end
# Max 20 commits from push DESC
def commits
@commits ||= (data[:commits] || []).reverse
end
def commit_title
commit = commits.last
commit[:message] if commit
end
def commit_id
commit_to || commit_from
end
def commits_count
data[:total_commits_count] || commits.count || 0
end
def ref_type
tag? ? "tag" : "branch"
end
def push_with_commits?
!commits.empty? && commit_from && commit_to
end
def last_push_to_non_root?
branch? && project.default_branch != branch_name
end
def target_iid
target.respond_to?(:iid) ? target.iid : target_id
end
......@@ -432,16 +352,6 @@ class Event < ActiveRecord::Base
user ? author_id == user.id : false
end
# We're manually replicating data into the new table since database triggers
# are not dumped to db/schema.rb. This could mean that a new installation
# would not have the triggers in place, thus losing events data in GitLab
# 10.0.
def replicate_event_for_push_events_migration
new_attributes = attributes.with_indifferent_access.except(:title, :data)
EventForMigration.create!(new_attributes)
end
def to_partial_path
# We are intentionally using `Event` rather than `self.class` so that
# subclasses also use the `Event` implementation.
......
# This model is used to replicate events between the old "events" table and the
# new "events_for_migration" table that will replace "events" in GitLab 10.0.
class EventForMigration < ActiveRecord::Base
self.table_name = 'events_for_migration'
end
......@@ -34,7 +34,8 @@ class Label < ActiveRecord::Base
scope :templates, -> { where(template: true) }
scope :with_title, ->(title) { where(title: title) }
scope :on_project_boards, ->(project_id) { joins(lists: :board).merge(List.movable).where(boards: { project_id: project_id }) }
scope :with_lists_and_board, -> { joins(lists: :board).merge(List.movable) }
scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) }
def self.prioritized(project)
joins(:priorities)
......@@ -172,6 +173,7 @@ class Label < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
json[:type] = self.try(:type)
json[:priority] = priority(options[:project]) if options.key?(:project)
end
end
......
......@@ -1511,6 +1511,14 @@ class Project < ActiveRecord::Base
end
end
def multiple_issue_boards_available?(user)
feature_available?(:multiple_issue_boards, user)
end
def issue_board_milestone_available?(user = nil)
feature_available?(:issue_board_milestone, user)
end
def full_path_was
File.join(namespace.full_path, previous_changes['path'].first)
end
......
......@@ -15,15 +15,21 @@ class PushEvent < Event
# should ensure the ID points to a valid project.
validates :project_id, presence: true
# The "data" field must not be set for push events since it's not used and a
# waste of space.
validates :data, absence: true
# These fields are also not used for push events, thus storing them would be a
# waste.
validates :target_id, absence: true
validates :target_type, absence: true
delegate :branch?, to: :push_event_payload
delegate :tag?, to: :push_event_payload
delegate :commit_from, to: :push_event_payload
delegate :commit_to, to: :push_event_payload
delegate :ref_type, to: :push_event_payload
delegate :commit_title, to: :push_event_payload
delegate :commit_count, to: :push_event_payload
alias_method :commits_count, :commit_count
def self.sti_name
PUSHED
end
......@@ -36,86 +42,35 @@ class PushEvent < Event
!!(commit_from && commit_to)
end
def tag?
return super unless push_event_payload
push_event_payload.tag?
end
def branch?
return super unless push_event_payload
push_event_payload.branch?
end
def valid_push?
return super unless push_event_payload
push_event_payload.ref.present?
end
def new_ref?
return super unless push_event_payload
push_event_payload.created?
end
def rm_ref?
return super unless push_event_payload
push_event_payload.removed?
end
def commit_from
return super unless push_event_payload
push_event_payload.commit_from
end
def commit_to
return super unless push_event_payload
push_event_payload.commit_to
def md_ref?
!(rm_ref? || new_ref?)
end
def ref_name
return super unless push_event_payload
push_event_payload.ref
end
def ref_type
return super unless push_event_payload
push_event_payload.ref_type
end
def branch_name
return super unless push_event_payload
ref_name
end
def tag_name
return super unless push_event_payload
ref_name
end
def commit_title
return super unless push_event_payload
push_event_payload.commit_title
end
alias_method :branch_name, :ref_name
alias_method :tag_name, :ref_name
def commit_id
commit_to || commit_from
end
def commits_count
return super unless push_event_payload
push_event_payload.commit_count
def last_push_to_non_root?
branch? && project.default_branch != branch_name
end
def validate_push_action
......
......@@ -35,6 +35,7 @@ class User < ActiveRecord::Base
default_value_for :project_view, :files
default_value_for :notified_of_own_activity, false
default_value_for :preferred_language, I18n.default_locale
default_value_for :theme_id, gitlab_config.default_theme
attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base,
......
module Boards
class BaseService < ::BaseService
# Parent can either a group or a project
attr_accessor :parent, :current_user, :params
def initialize(parent, user, params = {})
@parent, @current_user, @params = parent, user, params.dup
end
end
end
module Boards
class CreateService < BaseService
class CreateService < Boards::BaseService
def execute
create_board! if can_create_board?
end
......@@ -7,11 +7,11 @@ module Boards
private
def can_create_board?
project.boards.size == 0
parent.boards.size == 0
end
def create_board!
board = project.boards.create(params)
board = parent.boards.create(params)
if board.persisted?
board.lists.create(list_type: :backlog)
......
module Boards
module Issues
class CreateService < BaseService
class CreateService < Boards::BaseService
attr_accessor :project
def initialize(parent, project, user, params = {})
@project = project
super(parent, user, params)
end
def execute
create_issue(params.merge(label_ids: [list.label_id]))
end
......@@ -8,7 +16,7 @@ module Boards
private
def board
@board ||= project.boards.find(params.delete(:board_id))
@board ||= parent.boards.find(params.delete(:board_id))
end
def list
......
module Boards
module Issues
class ListService < BaseService
class ListService < Boards::BaseService
def execute
issues = IssuesFinder.new(current_user, filter_params).execute
issues = without_board_labels(issues) unless movable_list? || closed_list?
......@@ -11,7 +11,7 @@ module Boards
private
def board
@board ||= project.boards.find(params[:board_id])
@board ||= parent.boards.find(params[:board_id])
end
def list
......@@ -33,14 +33,14 @@ module Boards
end
def filter_params
set_project
set_parent
set_state
params
end
def set_project
params[:project_id] = project.id
def set_parent
params[:project_id] = parent.id
end
def set_state
......
module Boards
module Issues
class MoveService < BaseService
class MoveService < Boards::BaseService
def execute(issue)
return false unless can?(current_user, :update_issue, issue)
return false if issue_params.empty?
update_service.execute(issue)
update(issue)
end
private
def board
@board ||= project.boards.find(params[:board_id])
@board ||= parent.boards.find(params[:board_id])
end
def move_between_lists?
......@@ -27,8 +27,8 @@ module Boards
@moving_to_list ||= board.lists.find_by(id: params[:to_list_id])
end
def update_service
::Issues::UpdateService.new(project, current_user, issue_params)
def update(issue)
::Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue)
end
def issue_params
......@@ -42,7 +42,7 @@ module Boards
)
end
attrs[:move_between_iids] = move_between_iids if move_between_iids
attrs[:move_between_ids] = move_between_ids if move_between_ids
attrs
end
......@@ -61,16 +61,16 @@ module Boards
if moving_to_list.movable?
moving_from_list.label_id
else
Label.on_project_boards(project.id).pluck(:label_id)
Label.on_project_boards(parent.id).pluck(:label_id)
end
Array(label_ids).compact
end
def move_between_iids
return unless params[:move_after_iid] || params[:move_before_iid]
def move_between_ids
return unless params[:move_after_id] || params[:move_before_id]
[params[:move_after_iid], params[:move_before_iid]]
[params[:move_after_id], params[:move_before_id]]
end
end
end
......
module Boards
class ListService < BaseService
class ListService < Boards::BaseService
def execute
create_board! if project.boards.empty?
project.boards
create_board! if parent.boards.empty?
parent.boards
end
private
def create_board!
Boards::CreateService.new(project, current_user).execute
Boards::CreateService.new(parent, current_user).execute
end
end
end
module Boards
module Lists
class CreateService < BaseService
class CreateService < Boards::BaseService
def execute(board)
List.transaction do
label = available_labels.find(params[:label_id])
label = available_labels_for(board).find(params[:label_id])
position = next_position(board)
create_list(board, label, position)
end
end
private
def available_labels
LabelsFinder.new(current_user, project_id: project.id).execute
def available_labels_for(board)
LabelsFinder.new(current_user, project_id: parent.id).execute
end
def next_position(board)
......
module Boards
module Lists
class DestroyService < BaseService
class DestroyService < Boards::BaseService
def execute(list)
return false unless list.destroyable?
......
module Boards
module Lists
class GenerateService < BaseService
class GenerateService < Boards::BaseService
def execute(board)
return false unless board.lists.movable.empty?
......@@ -15,11 +15,11 @@ module Boards
def create_list(board, params)
label = find_or_create_label(params)
Lists::CreateService.new(project, current_user, label_id: label.id).execute(board)
Lists::CreateService.new(parent, current_user, label_id: label.id).execute(board)
end
def find_or_create_label(params)
::Labels::FindOrCreateService.new(current_user, project, params).execute
::Labels::FindOrCreateService.new(current_user, parent, params).execute
end
def label_params
......
module Boards
module Lists
class ListService < BaseService
class ListService < Boards::BaseService
def execute(board)
board.lists.create(list_type: :backlog) unless board.lists.backlog.exists?
......
module Boards
module Lists
class MoveService < BaseService
class MoveService < Boards::BaseService
def execute(list)
@board = list.board
@old_position = list.position
......
......@@ -3,7 +3,7 @@ module Issues
include SpamCheckService
def execute(issue)
handle_move_between_iids(issue)
handle_move_between_ids(issue)
filter_spam_check_params
change_issue_duplicate(issue)
move_issue_to_new_project(issue) || update(issue)
......@@ -54,13 +54,13 @@ module Issues
end
end
def handle_move_between_iids(issue)
return unless params[:move_between_iids]
def handle_move_between_ids(issue)
return unless params[:move_between_ids]
after_iid, before_iid = params.delete(:move_between_iids)
after_id, before_id = params.delete(:move_between_ids)
issue_before = get_issue_if_allowed(issue.project, before_iid) if before_iid
issue_after = get_issue_if_allowed(issue.project, after_iid) if after_iid
issue_before = get_issue_if_allowed(issue.project, before_id) if before_id
issue_after = get_issue_if_allowed(issue.project, after_id) if after_id
issue.move_between(issue_before, issue_after)
end
......@@ -87,8 +87,8 @@ module Issues
private
def get_issue_if_allowed(project, iid)
issue = project.issues.find_by(iid: iid)
def get_issue_if_allowed(project, id)
issue = project.issues.find(id)
issue if can?(current_user, :update_issue, issue)
end
......
!!! 5
%html{ lang: I18n.locale, class: page_class }
= render "layouts/head"
%body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } }
%body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } }
= render "layouts/init_auto_complete" if @gfm_form
= render 'peek/bar'
= render "layouts/header/default"
......
%ul.list-unstyled.navbar-sub-nav
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown" }) do
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects" }) do
%a{ href: "#", data: { toggle: "dropdown" } }
Projects
= custom_icon('caret_down')
......@@ -22,7 +22,7 @@
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
Snippets
%li.dropdown.hidden-lg
%li.header-more.dropdown.hidden-lg
%a{ href: "#", data: { toggle: "dropdown" } }
More
= custom_icon('caret_down')
......
......@@ -14,6 +14,11 @@
Overview
%ul.sidebar-sub-level-items
= nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: { class: "fly-out-top-item" } ) do
= link_to admin_root_path do
%strong.fly-out-top-item-name
#{ _('Overview') }
%li.divider.fly-out-top-item
= nav_link(controller: :dashboard, html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview' do
%span
......@@ -55,6 +60,11 @@
Monitoring
%ul.sidebar-sub-level-items
= nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles), html_options: { class: "fly-out-top-item" } ) do
= link_to admin_conversational_development_index_path do
%strong.fly-out-top-item-name
#{ _('Monitoring') }
%li.divider.fly-out-top-item
= nav_link(controller: :system_info) do
= link_to admin_system_info_path, title: 'System Info' do
%span
......@@ -82,6 +92,11 @@
= custom_icon('messages')
%span.nav-item-name
Messages
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :broadcast_messages, html_options: { class: "fly-out-top-item" } ) do
= link_to admin_broadcast_messages_path do
%strong.fly-out-top-item-name
#{ _('Messages') }
= nav_link(controller: [:hooks, :hook_logs]) do
= sidebar_link admin_hooks_path, title: _('Hooks') do
......@@ -89,6 +104,11 @@
= custom_icon('system_hooks')
%span.nav-item-name
System Hooks
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: [:hooks, :hook_logs], html_options: { class: "fly-out-top-item" } ) do
= link_to admin_hooks_path do
%strong.fly-out-top-item-name
#{ _('System Hooks') }
= nav_link(controller: :applications) do
= sidebar_link admin_applications_path, title: _('Applications') do
......@@ -96,6 +116,11 @@
= custom_icon('applications')
%span.nav-item-name
Applications
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :applications, html_options: { class: "fly-out-top-item" } ) do
= link_to admin_applications_path do
%strong.fly-out-top-item-name
#{ _('Applications') }
= nav_link(controller: :abuse_reports) do
= sidebar_link admin_abuse_reports_path, title: _("Abuse Reports") do
......@@ -104,6 +129,12 @@
%span.nav-item-name
Abuse Reports
%span.badge.count= number_with_delimiter(AbuseReport.count(:all))
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :abuse_reports, html_options: { class: "fly-out-top-item" } ) do
= link_to admin_broadcast_messages_path do
%strong.fly-out-top-item-name
#{ _('Abuse Reports') }
%span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(AbuseReport.count(:all))
- if akismet_enabled?
= nav_link(controller: :spam_logs) do
......@@ -112,6 +143,11 @@
= custom_icon('spam_logs')
%span.nav-item-name
Spam Logs
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :spam_logs, html_options: { class: "fly-out-top-item" } ) do
= link_to admin_spam_logs_path do
%strong.fly-out-top-item-name
#{ _('Spam Logs') }
= nav_link(controller: :deploy_keys) do
= sidebar_link admin_deploy_keys_path, title: _('Deploy Keys') do
......@@ -119,6 +155,11 @@
= custom_icon('key')
%span.nav-item-name
Deploy Keys
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :deploy_keys, html_options: { class: "fly-out-top-item" } ) do
= link_to admin_deploy_keys_path do
%strong.fly-out-top-item-name
#{ _('Deploy Keys') }
= nav_link(controller: :services) do
= sidebar_link admin_application_settings_services_path, title: _('Service Templates') do
......@@ -126,6 +167,11 @@
= custom_icon('service_templates')
%span.nav-item-name
Service Templates
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :services, html_options: { class: "fly-out-top-item" } ) do
= link_to admin_application_settings_services_path do
%strong.fly-out-top-item-name
#{ _('Service Templates') }
= nav_link(controller: :labels) do
= sidebar_link admin_labels_path, title: _('Labels') do
......@@ -133,6 +179,11 @@
= custom_icon('labels')
%span.nav-item-name
Labels
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :labels, html_options: { class: "fly-out-top-item" } ) do
= link_to admin_labels_path do
%strong.fly-out-top-item-name
#{ _('Labels') }
= nav_link(controller: :appearances) do
= sidebar_link admin_appearances_path, title: _('Appearances') do
......@@ -140,6 +191,11 @@
= custom_icon('appearance')
%span.nav-item-name
Appearance
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :appearances, html_options: { class: "fly-out-top-item" } ) do
= link_to admin_appearances_path do
%strong.fly-out-top-item-name
#{ _('Appearance') }
= nav_link(controller: :application_settings) do
= sidebar_link admin_application_settings_path, title: _('Settings') do
......@@ -147,5 +203,10 @@
= custom_icon('settings')
%span.nav-item-name
Settings
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :application_settings, html_options: { class: "fly-out-top-item" } ) do
= link_to admin_application_settings_path do
%strong.fly-out-top-item-name
#{ _('Settings') }
= render 'shared/sidebar_toggle_button'
- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
.context-header
......@@ -15,6 +18,11 @@
Overview
%ul.sidebar-sub-level-items
= nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: "fly-out-top-item" } ) do
= link_to group_path(@group) do
%strong.fly-out-top-item-name
#{ _('Overview') }
%li.divider.fly-out-top-item
= nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do
= link_to group_path(@group), title: 'Group details' do
%span
......@@ -30,11 +38,16 @@
.nav-icon-container
= custom_icon('issues')
%span.nav-item-name
- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
Issues
%span.badge.count= number_with_delimiter(issues.count)
%ul.sidebar-sub-level-items
= nav_link(path: ['groups#issues', 'labels#index', 'milestones#index'], html_options: { class: "fly-out-top-item" } ) do
= link_to issues_group_path(@group) do
%strong.fly-out-top-item-name
#{ _('Issues') }
%span.badge.count.issue_counter.fly-out-badge= number_with_delimiter(issues.count)
%li.divider.fly-out-top-item
= nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
= link_to issues_group_path(@group), title: 'List' do
%span
......@@ -55,15 +68,25 @@
.nav-icon-container
= custom_icon('mr_bold')
%span.nav-item-name
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
Merge Requests
%span.badge.count= number_with_delimiter(merge_requests.count)
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do
= link_to merge_requests_group_path(@group) do
%strong.fly-out-top-item-name
#{ _('Merge Requests') }
%span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests.count)
= nav_link(path: 'group_members#index') do
= sidebar_link group_group_members_path(@group), title: _('Members') do
.nav-icon-container
= custom_icon('members')
%span.nav-item-name
Members
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: 'group_members#index', html_options: { class: "fly-out-top-item" } ) do
= link_to merge_requests_group_path(@group) do
%strong.fly-out-top-item-name
#{ _('Members') }
- if current_user && can?(current_user, :admin_group, @group)
= nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do
= sidebar_link edit_group_path(@group), title: _('Settings') do
......@@ -72,6 +95,11 @@
%span.nav-item-name
Settings
%ul.sidebar-sub-level-items
= nav_link(path: %w[groups#projects groups#edit ci_cd#show], html_options: { class: "fly-out-top-item" } ) do
= link_to edit_group_path(@group) do
%strong.fly-out-top-item-name
#{ _('Settings') }
%li.divider.fly-out-top-item
= nav_link(path: 'groups#edit') do
= link_to edit_group_path(@group), title: 'General' do
%span
......
......@@ -12,12 +12,22 @@
= custom_icon('profile')
%span.nav-item-name
Profile
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: 'profiles#show', html_options: { class: "fly-out-top-item" } ) do
= link_to profile_path do
%strong.fly-out-top-item-name
#{ _('Profile') }
= nav_link(controller: [:accounts, :two_factor_auths]) do
= sidebar_link profile_account_path, title: _('Account') do
.nav-icon-container
= custom_icon('account')
%span.nav-item-name
Account
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: [:accounts, :two_factor_auths], html_options: { class: "fly-out-top-item" } ) do
= link_to profile_account_path do
%strong.fly-out-top-item-name
#{ _('Account') }
- if current_application_settings.user_oauth_applications?
= nav_link(controller: 'oauth/applications') do
= sidebar_link applications_profile_path, title: _('Applications') do
......@@ -25,24 +35,44 @@
= custom_icon('applications')
%span.nav-item-name
Applications
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: 'oauth/applications', html_options: { class: "fly-out-top-item" } ) do
= link_to applications_profile_path do
%strong.fly-out-top-item-name
#{ _('Applications') }
= nav_link(controller: :chat_names) do
= sidebar_link profile_chat_names_path, title: _('Chat') do
.nav-icon-container
= custom_icon('chat')
%span.nav-item-name
Chat
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :chat_names, html_options: { class: "fly-out-top-item" } ) do
= link_to profile_chat_names_path do
%strong.fly-out-top-item-name
#{ _('Chat') }
= nav_link(controller: :personal_access_tokens) do
= sidebar_link profile_personal_access_tokens_path, title: _('Access Tokens') do
.nav-icon-container
= custom_icon('access_tokens')
%span.nav-item-name
Access Tokens
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :personal_access_tokens, html_options: { class: "fly-out-top-item" } ) do
= link_to profile_personal_access_tokens_path do
%strong.fly-out-top-item-name
#{ _('Access Tokens') }
= nav_link(controller: :emails) do
= sidebar_link profile_emails_path, title: _('Emails') do
.nav-icon-container
= custom_icon('emails')
%span.nav-item-name
Emails
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :emails, html_options: { class: "fly-out-top-item" } ) do
= link_to profile_emails_path do
%strong.fly-out-top-item-name
#{ _('Emails') }
- unless current_user.ldap_user?
= nav_link(controller: :passwords) do
= sidebar_link edit_profile_password_path, title: _('Password') do
......@@ -50,36 +80,65 @@
= custom_icon('lock')
%span.nav-item-name
Password
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :passwords, html_options: { class: "fly-out-top-item" } ) do
= link_to edit_profile_password_path do
%strong.fly-out-top-item-name
#{ _('Password') }
= nav_link(controller: :notifications) do
= sidebar_link profile_notifications_path, title: _('Notifications') do
.nav-icon-container
= custom_icon('notifications')
%span.nav-item-name
Notifications
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :notifications, html_options: { class: "fly-out-top-item" } ) do
= link_to profile_notifications_path do
%strong.fly-out-top-item-name
#{ _('Notifications') }
= nav_link(controller: :keys) do
= sidebar_link profile_keys_path, title: _('SSH Keys') do
.nav-icon-container
= custom_icon('key')
%span.nav-item-name
SSH Keys
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :keys, html_options: { class: "fly-out-top-item" } ) do
= link_to profile_keys_path do
%strong.fly-out-top-item-name
#{ _('SSH Keys') }
= nav_link(controller: :gpg_keys) do
= sidebar_link profile_gpg_keys_path, title: _('GPG Keys') do
.nav-icon-container
= custom_icon('key_2')
%span.nav-item-name
GPG Keys
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :gpg_keys, html_options: { class: "fly-out-top-item" } ) do
= link_to profile_gpg_keys_path do
%strong.fly-out-top-item-name
#{ _('GPG Keys') }
= nav_link(controller: :preferences) do
= sidebar_link profile_preferences_path, title: _('Preferences') do
.nav-icon-container
= custom_icon('preferences')
%span.nav-item-name
Preferences
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :preferences, html_options: { class: "fly-out-top-item" } ) do
= link_to profile_preferences_path do
%strong.fly-out-top-item-name
#{ _('Preferences') }
= nav_link(path: 'profiles#audit_log') do
= sidebar_link audit_log_profile_path, title: _('Authentication log') do
.nav-icon-container
= custom_icon('authentication_log')
%span.nav-item-name
Authentication log
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: 'profiles#audit_log', html_options: { class: "fly-out-top-item" } ) do
= link_to audit_log_profile_path do
%strong.fly-out-top-item-name
#{ _('Authentication Log') }
= render 'shared/sidebar_toggle_button'
......@@ -16,6 +16,11 @@
Overview
%ul.sidebar-sub-level-items
= nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do
= link_to project_path(@project) do
%strong.fly-out-top-item-name
#{ _('Overview') }
%li.divider.fly-out-top-item
= nav_link(path: 'projects#show') do
= link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do
%span= _('Details')
......@@ -38,6 +43,11 @@
Repository
%ul.sidebar-sub-level-items
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network), html_options: { class: "fly-out-top-item" } ) do
= link_to project_tree_path(@project) do
%strong.fly-out-top-item-name
#{ _('Repository') }
%li.divider.fly-out-top-item
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
= link_to project_tree_path(@project) do
#{ _('Files') }
......@@ -90,6 +100,14 @@
= number_with_delimiter(@project.open_issues_count)
%ul.sidebar-sub-level-items
= nav_link(controller: :issues, html_options: { class: "fly-out-top-item" } ) do
= link_to project_issues_path(@project) do
%strong.fly-out-top-item-name
#{ _('Issues') }
- if @project.issues_enabled?
%span.badge.count.issue_counter.fly-out-badge
= number_with_delimiter(@project.open_issues_count)
%li.divider.fly-out-top-item
= nav_link(controller: :issues) do
= link_to project_issues_path(@project), title: 'Issues' do
%span
......@@ -133,6 +151,13 @@
Merge Requests
%span.badge.count.merge_counter.js-merge-counter
= number_with_delimiter(@project.open_merge_requests_count)
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :merge_requests, html_options: { class: "fly-out-top-item" } ) do
= link_to project_merge_requests_path(@project) do
%strong.fly-out-top-item-name
#{ _('Merge Requests') }
%span.badge.count.merge_counter.js-merge-counter.fly-out-badge
= number_with_delimiter(@project.open_merge_requests_count)
- if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do
......@@ -143,6 +168,11 @@
CI / CD
%ul.sidebar-sub-level-items
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts], html_options: { class: "fly-out-top-item" } ) do
= link_to project_pipelines_path(@project) do
%strong.fly-out-top-item-name
#{ _('CI / CD') }
%li.divider.fly-out-top-item
- if project_nav_tab? :pipelines
= nav_link(path: ['pipelines#index', 'pipelines#show']) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
......@@ -180,6 +210,11 @@
= custom_icon('wiki')
%span.nav-item-name
Wiki
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :wikis, html_options: { class: "fly-out-top-item" } ) do
= link_to get_project_wiki_path(@project) do
%strong.fly-out-top-item-name
#{ _('Wiki') }
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
......@@ -188,6 +223,11 @@
= custom_icon('snippets')
%span.nav-item-name
Snippets
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do
= link_to project_snippets_path(@project) do
%strong.fly-out-top-item-name
#{ _('Snippets') }
- if project_nav_tab? :settings
= nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do
......@@ -200,6 +240,11 @@
%ul.sidebar-sub-level-items
- can_edit = can?(current_user, :admin_project, @project)
- if can_edit
= nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show], html_options: { class: "fly-out-top-item" } ) do
= link_to edit_project_path(@project) do
%strong.fly-out-top-item-name
#{ _('Settings') }
%li.divider.fly-out-top-item
= nav_link(path: %w[projects#edit]) do
= link_to edit_project_path(@project), title: 'General' do
%span
......
......@@ -3,6 +3,26 @@
= render 'profiles/head'
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f|
.col-lg-4.application-theme
%h4.prepend-top-0
GitLab navigation theme
%p Customize the appearance of the application header and navigation sidebar.
.col-lg-8.application-theme
- Gitlab::Themes.each do |theme|
= label_tag do
.preview{ class: theme.name.downcase }
.preview-row
.quadrant.one
.quadrant.two
.preview-row
.quadrant.three
.quadrant.four
= f.radio_button :theme_id, theme.id
= theme.name
.col-sm-12
%hr
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Syntax highlighting theme
......
// Remove body class for any previous theme, re-add current one
$('body').removeClass('<%= Gitlab::Themes.body_classes %>')
$('body').addClass('<%= user_application_theme %>')
// Toggle container-fluid class
if ('<%= current_user.layout %>' === 'fluid') {
$('.content-wrapper .container-fluid').removeClass('container-limited')
......
= render "show"
= render "shared/boards/show", board: @boards.first
= render "show"
= render "shared/boards/show", board: @board
......@@ -28,8 +28,8 @@
= link_to icon('question-circle'), help_page_path("gitlab-basics/create-project"), target: '_blank', aria: { label: "What’s included in a template?" }, title: "What’s included in a template?", class: 'has-tooltip', data: { placement: 'top'}
%div
= render 'project_templates', f: f
.second-column
- if import_sources_enabled?
.second-column
.project-import
.form-group.clearfix
= f.label :visibility_level, class: 'label-light' do #the label here seems wrong
......
......@@ -27,9 +27,10 @@
= link_to project_tags_path(@project) do
#{n_('Tag', 'Tags', @repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)})
- if default_project_view != 'readme' && @repository.readme
- if @repository.readme
%li
= link_to _('Readme'), readme_path(@project)
= link_to _('Readme'),
default_project_view != 'readme' ? readme_path(@project) : '#readme'
- if @repository.changelog
%li
......
- if readme.rich_viewer
%article.file-holder.readme-holder{ class: ("limited-width-container" unless fluid_layout) }
%article.file-holder.readme-holder{ id: 'readme', class: ("limited-width-container" unless fluid_layout) }
.js-file-title.file-title
= blob_icon readme.mode, readme.name
= link_to project_blob_path(@project, tree_join(@ref, readme.path)) do
......
......@@ -9,7 +9,7 @@
= webpack_bundle_tag 'filtered_search'
= webpack_bundle_tag 'boards'
%script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
%script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board"
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
= render "projects/issues/head"
......@@ -30,7 +30,7 @@
":root-path" => "rootPath",
":board-id" => "boardId",
":key" => "_uid" }
= render "projects/boards/components/sidebar"
= render "shared/boards/components/sidebar"
%board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'),
"new-issue-path" => new_project_issue_path(@project),
"milestone-path" => milestones_filter_dropdown_path,
......
......@@ -7,20 +7,26 @@
":class": "{ \"fa-caret-down\": list.isExpanded, \"fa-caret-right\": !list.isExpanded && list.position === -1, \"fa-caret-left\": !list.isExpanded && list.position !== -1 }",
"aria-hidden": "true" }
%span.has-tooltip{ "v-if": "list.type !== \"label\"",
%span.board-title-text.has-tooltip{ "v-if": "list.type !== \"label\"",
":title" => '(list.label ? list.label.description : "")' }
{{ list.title }}
%span.has-tooltip{ "v-if": "list.type === \"label\"",
":title" => '(list.label ? list.label.description : "")',
data: { container: "body", placement: "bottom" },
class: "label color-label title",
class: "label color-label title board-title-text",
":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.color ? list.label.text_color : \"#2e2e2e\") }" }
{{ list.title }}
.issue-count-badge.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' }
- if can?(current_user, :admin_list, current_board_parent)
%board-delete{ "inline-template" => true,
":list" => "list",
"v-if" => "!list.preset && list.id" }
%button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash")
.issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank"' }
%span.issue-count-badge-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
{{ list.issuesSize }}
- if can?(current_user, :admin_issue, @project)
- if can?(current_user, :admin_list, current_board_parent)
%button.issue-count-badge-add-button.btn.btn-small.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button",
"@click" => "showNewIssueForm",
"v-if" => 'list.type !== "closed"',
......@@ -28,12 +34,7 @@
"title" => "New issue",
data: { placement: "top", container: "body" } }
= icon("plus", class: "js-no-trigger-collapse")
- if can?(current_user, :admin_list, @project)
%board-delete{ "inline-template" => true,
":list" => "list",
"v-if" => "!list.preset && list.id" }
%button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash")
%board-list{ "v-if" => 'list.type !== "blank"',
":list" => "list",
":issues" => "list.issues",
......@@ -42,5 +43,5 @@
":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath",
"ref" => "board-list" }
- if can?(current_user, :admin_list, @project)
- if can?(current_user, :admin_list, current_board_parent)
%board-blank-state{ "v-if" => 'list.id == "blank"' }
......@@ -10,18 +10,19 @@
%br/
%span
= precede "#" do
{{ issue.id }}
{{ issue.iid }}
%a.gutter-toggle.pull-right{ role: "button",
href: "#",
"@click.prevent" => "closeSidebar",
"aria-label" => "Toggle sidebar" }
= custom_icon("icon_close", size: 15)
.js-issuable-update
= render "projects/boards/components/sidebar/assignee"
= render "projects/boards/components/sidebar/milestone"
= render "projects/boards/components/sidebar/due_date"
= render "projects/boards/components/sidebar/labels"
= render "projects/boards/components/sidebar/notifications"
= render "shared/boards/components/sidebar/assignee"
= render "shared/boards/components/sidebar/milestone"
= render "shared/boards/components/sidebar/due_date"
= render "shared/boards/components/sidebar/labels"
= render "shared/boards/components/sidebar/notifications"
%remove-btn{ ":issue" => "issue",
":issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'",
":list" => "list",
"v-if" => "canRemove" }
......@@ -2,13 +2,13 @@
%template{ "v-if" => "issue.assignees" }
%assignee-title{ ":number-of-assignees" => "issue.assignees.length",
":loading" => "loadingAssignees",
":editable" => can?(current_user, :admin_issue, @project) }
":editable" => can_admin_issue? }
%assignees.value{ "root-path" => "#{root_url}",
":users" => "issue.assignees",
":editable" => can?(current_user, :admin_issue, @project),
":editable" => can_admin_issue?,
"@assign-self" => "assignSelf" }
- if can?(current_user, :admin_issue, @project)
- if can_admin_issue?
.selectbox.hide-collapsed
%input.js-vue{ type: "hidden",
name: "issue[assignee_ids][]",
......@@ -20,9 +20,9 @@
":data-username" => "assignee.username" }
.dropdown
- dropdown_options = issue_assignees_dropdown_options
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: { toggle: 'dropdown', field_name: 'issue[assignee_ids][]', first_user: current_user&.username, current_user: 'true', project_id: @project.id, null_user: 'true', multi_select: 'true', 'dropdown-header': dropdown_options[:data][:'dropdown-header'], 'max-select': dropdown_options[:data][:'max-select'] },
":data-issuable-id" => "issue.id",
":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" }
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: board_sidebar_user_data,
":data-issuable-id" => "issue.iid",
":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
= dropdown_options[:title]
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
......
.block.due_date
.title
Due date
- if can?(current_user, :admin_issue, @project)
- if can_admin_issue?
= icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
.value
......@@ -10,12 +10,12 @@
No due date
%span.bold{ "v-if" => "issue.dueDate" }
{{ issue.dueDate | due-date }}
- if can?(current_user, :admin_issue, @project)
- if can_admin_issue?
%span.no-value.js-remove-due-date-holder{ "v-if" => "issue.dueDate" }
\-
%a.js-remove-due-date{ href: "#", role: "button" }
remove due date
- if can?(current_user, :admin_issue, @project)
- if can_admin_issue?
.selectbox
%input{ type: "hidden",
name: "issue[due_date]",
......@@ -23,7 +23,7 @@
.dropdown
%button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button',
data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" },
":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" }
":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
%span.dropdown-toggle-text Due date
= icon('chevron-down')
.dropdown-menu.dropdown-menu-due-date
......
.block.labels
.title
Labels
- if can?(current_user, :admin_issue, @project)
- if can_admin_issue?
= icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
.value.issuable-show-labels
......@@ -11,7 +11,7 @@
"v-for" => "label in issue.labels" }
%span.label.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
{{ label.title }}
- if can?(current_user, :admin_issue, @project)
- if can_admin_issue?
.selectbox
%input{ type: "hidden",
name: "issue[label_names][]",
......@@ -19,12 +19,19 @@
":value" => "label.id" }
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button",
data: { toggle: "dropdown", field_name: "issue[label_names][]", show_no: "true", show_any: "true", project_id: @project.id, labels: project_labels_path(@project, :json), namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path) },
":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" }
data: { toggle: "dropdown",
field_name: "issue[label_names][]",
show_no: "true",
show_any: "true",
project_id: @project&.try(:id),
labels: labels_filter_path(false),
namespace_path: @project.try(:namespace).try(:full_path),
project_path: @project.try(:path) },
":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
%span.dropdown-toggle-text
Label
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default"
- if can? current_user, :admin_label, @project and @project
- if can?(current_user, :admin_label, current_board_parent)
= render partial: "shared/issuable/label_page_create"
.block.milestone
.title
Milestone
- if can?(current_user, :admin_issue, @project)
- if can_admin_issue?
= icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
.value
......@@ -9,17 +9,17 @@
None
%span.bold.has-tooltip{ "v-if" => "issue.milestone" }
{{ issue.milestone.title }}
- if can?(current_user, :admin_issue, @project)
- if can_admin_issue?
.selectbox
%input{ type: "hidden",
":value" => "issue.milestone.id",
name: "issue[milestone_id]",
"v-if" => "issue.milestone" }
.dropdown
%button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", project_id: @project.id, milestones: project_milestones_path(@project, :json), ability_name: "issue", use_id: "true", default_no: "true" },
%button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", milestones: milestones_filter_path(format: :json), ability_name: "issue", use_id: "true", default_no: "true" },
":data-selected" => "milestoneTitle",
":data-issuable-id" => "issue.id",
":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" }
":data-issuable-id" => "issue.iid",
":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
Milestone
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable
......
- if current_user
.block.light.subscription{ ":data-url" => "'#{project_issues_path(@project)}/' + issue.id + '/toggle_subscription'" }
.block.light.subscription{ ":data-url" => "'#{build_issue_link_base}/' + issue.iid + '/toggle_subscription'" }
%span.issuable-header-text.hide-collapsed.pull-left
Notifications
%button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
......
......@@ -8,20 +8,19 @@
- if show_boards_content
.issue-board-dropdown-content
%p
Create lists from the labels you use in your project. Issues with that
label will automatically be added to the list.
Create lists from labels. Issues with that label appear in that list.
= dropdown_filter(filter_placeholder)
= dropdown_content
- if @project && show_footer
- if current_board_parent && show_footer
= dropdown_footer do
%ul.dropdown-footer-list
- if can?(current_user, :admin_label, @project)
- if can?(current_user, :admin_label, current_board_parent)
%li
%a.dropdown-toggle-page{ href: "#" }
Create new label
%li
= link_to project_labels_path(@project), :"data-is-link" => true do
- if show_create && @project && can?(current_user, :admin_label, @project)
= link_to labels_path, :"data-is-link" => true do
- if show_create && can?(current_user, :admin_label, current_board_parent)
Manage labels
- else
View labels
......
......@@ -104,13 +104,13 @@
= icon('times')
.filter-dropdown-container
- if type == :boards
- if can?(current_user, :admin_list, @project)
- if can?(current_user, :admin_list, board.parent)
.dropdown.prepend-left-10#js-add-list
%button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path) } }
%button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data }
Add list
.dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
- if can?(current_user, :admin_label, @project)
- if can?(current_user, :admin_label, board.parent)
= render partial: "shared/issuable/label_page_create"
= dropdown_loading
#js-add-issues-btn.prepend-left-10
......
---
title: Add div id to the readme in the project overview
merge_request: 13735
author: Riccardo Padovani @rpadovani
type: added
---
title: Add option in preferences to change navigation theme color
merge_request:
author:
type: added
---
title: Finish migration to the new events setup
merge_request:
author:
type: changed
---
title: Add documentation for PlantUML in reStructuredText
merge_request: 13900
author: Markus Koller
type: other
---
title: Fix stray OR in New Project page
merge_request: 14096
author: Robin Bobbitt
type: fixed
......@@ -76,6 +76,13 @@ production: &base
# default_can_create_group: false # default: true
# username_changing_enabled: false # default: true - User can change her username/namespace
## Default theme ID
## 1 - Indigo
## 2 - Dark
## 3 - Light
## 4 - Blue
## 5 - Green
# default_theme: 1 # default: 1
## Automatic issue closing
# If a commit message matches this regular expression, all issues referenced from the matched text will be closed.
......
......@@ -232,6 +232,7 @@ Settings['gitlab'] ||= Settingslogic.new({})
Settings.gitlab['default_projects_limit'] ||= 100000
Settings.gitlab['default_branch_protection'] ||= 2
Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil?
Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil?
Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost'
Settings.gitlab['ssh_host'] ||= Settings.gitlab.host
Settings.gitlab['https'] = false if Settings.gitlab['https'].nil?
......
......@@ -74,6 +74,19 @@ Rails.application.routes.draw do
# Notification settings
resources :notification_settings, only: [:create, :update]
# Boards resources shared between group and projects
resources :boards do
resources :lists, module: :boards, only: [:index, :create, :update, :destroy] do
collection do
post :generate
end
resources :issues, only: [:index, :create, :update]
end
resources :issues, module: :boards, only: [:index, :update]
end
draw :import
draw :uploads
draw :explore
......
......@@ -343,19 +343,7 @@ constraints(ProjectUrlConstrainer.new) do
get 'noteable/:target_type/:target_id/notes' => 'notes#index', as: 'noteable_notes'
resources :boards, only: [:index, :show] do
scope module: :boards do
resources :issues, only: [:index, :update]
resources :lists, only: [:index, :create, :update, :destroy] do
collection do
post :generate
end
resources :issues, only: [:index, :create]
end
end
end
resources :boards, only: [:index, :show, :create, :update, :destroy]
resources :todos, only: [:create]
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddThemeIdToUsers < ActiveRecord::Migration
DOWNTIME = false
def change
add_column :users, :theme_id, :integer, limit: 2
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class StealRemainingEventMigrationJobs < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def up
Gitlab::BackgroundMigration.steal('MigrateEventsToPushEventPayloads')
end
def down
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class SwapEventMigrationTables < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def up
rename_tables
end
def down
rename_tables
end
def rename_tables
rename_table :events, :events_old
rename_table :events_for_migration, :events
rename_table :events_old, :events_for_migration
end
end
......@@ -7,6 +7,5 @@ class LimitsToMysql < ActiveRecord::Migration
change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647
change_column :snippets, :content, :text, limit: 2147483647
change_column :notes, :st_diff, :text, limit: 2147483647
change_column :events, :data, :text, limit: 2147483647
end
end
......@@ -54,14 +54,14 @@ class UpdateRetriedForCiBuild < ActiveRecord::Migration
def with_temporary_partial_index
if Gitlab::Database.postgresql?
unless index_exists?(:ci_builds, name: :index_for_ci_builds_retried_migration)
unless index_exists?(:ci_builds, :id, name: :index_for_ci_builds_retried_migration)
execute 'CREATE INDEX CONCURRENTLY index_for_ci_builds_retried_migration ON ci_builds (id) WHERE retried IS NULL;'
end
end
yield
if Gitlab::Database.postgresql? && index_exists?(:ci_builds, name: :index_for_ci_builds_retried_migration)
if Gitlab::Database.postgresql? && index_exists?(:ci_builds, :id, name: :index_for_ci_builds_retried_migration)
execute 'DROP INDEX CONCURRENTLY index_for_ci_builds_retried_migration'
end
end
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class DropEventsForMigrationTable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
class Event < ActiveRecord::Base
include EachBatch
end
def up
transaction do
drop_table :events_for_migration
end
end
# rubocop: disable Migration/Datetime
def down
create_table :events_for_migration do |t|
t.string :target_type, index: true
t.integer :target_id, index: true
t.string :title
t.text :data
t.integer :project_id
t.datetime :created_at, index: true
t.datetime :updated_at
t.integer :action, index: true
t.integer :author_id, index: true
t.index [:project_id, :id]
end
Event.all.each_batch do |relation|
start_id, stop_id = relation.pluck('MIN(id), MAX(id)').first
execute <<-EOF.strip_heredoc
INSERT INTO events_for_migration (target_type, target_id, project_id, created_at, updated_at, action, author_id)
SELECT target_type, target_id, project_id, created_at, updated_at, action, author_id
FROM events
WHERE id BETWEEN #{start_id} AND #{stop_id}
EOF
end
end
end
......@@ -32,8 +32,8 @@ ActiveRecord::Schema.define(version: 20170905112933) do
t.text "description", null: false
t.string "header_logo"
t.string "logo"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.text "description_html"
t.integer "cached_markdown_version"
end
......@@ -101,6 +101,10 @@ ActiveRecord::Schema.define(version: 20170905112933) do
t.text "help_page_text_html"
t.text "shared_runners_text_html"
t.text "after_sign_up_text_html"
t.integer "rsa_key_restriction", default: 0, null: false
t.integer "dsa_key_restriction", default: 0, null: false
t.integer "ecdsa_key_restriction", default: 0, null: false
t.integer "ed25519_key_restriction", default: 0, null: false
t.boolean "housekeeping_enabled", default: true, null: false
t.boolean "housekeeping_bitmaps_enabled", default: true, null: false
t.integer "housekeeping_incremental_repack_period", default: 10, null: false
......@@ -130,10 +134,6 @@ ActiveRecord::Schema.define(version: 20170905112933) do
t.boolean "hashed_storage_enabled", default: false, null: false
t.boolean "project_export_enabled", default: true, null: false
t.boolean "auto_devops_enabled", default: false, null: false
t.integer "rsa_key_restriction", default: 0, null: false
t.integer "dsa_key_restriction", default: 0, null: false
t.integer "ecdsa_key_restriction", default: 0, null: false
t.integer "ed25519_key_restriction", default: 0, null: false
end
create_table "audit_events", force: :cascade do |t|
......@@ -256,6 +256,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree
add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
add_index "ci_builds", ["id"], name: "index_for_ci_builds_retried_migration", where: "(retried IS NULL)", using: :btree, opclasses: {"id)"=>"WHERE"}
add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
add_index "ci_builds", ["protected"], name: "index_ci_builds_on_protected", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
......@@ -274,8 +275,8 @@ ActiveRecord::Schema.define(version: 20170905112933) do
t.string "encrypted_value_iv"
t.integer "group_id", null: false
t.boolean "protected", default: false, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
end
add_index "ci_group_variables", ["group_id", "key"], name: "index_ci_group_variables_on_group_id_and_key", unique: true, using: :btree
......@@ -287,8 +288,8 @@ ActiveRecord::Schema.define(version: 20170905112933) do
t.string "encrypted_value_salt"
t.string "encrypted_value_iv"
t.integer "pipeline_schedule_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.datetime_with_timezone "created_at"
t.datetime_with_timezone "updated_at"
end
add_index "ci_pipeline_schedule_variables", ["pipeline_schedule_id", "key"], name: "index_ci_pipeline_schedule_variables_on_schedule_id_and_key", unique: true, using: :btree
......@@ -533,38 +534,19 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_index "environments", ["project_id", "slug"], name: "index_environments_on_project_id_and_slug", unique: true, using: :btree
create_table "events", force: :cascade do |t|
t.string "target_type"
t.integer "target_id"
t.string "title"
t.text "data"
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "action"
t.integer "author_id"
end
add_index "events", ["action"], name: "index_events_on_action", using: :btree
add_index "events", ["author_id"], name: "index_events_on_author_id", using: :btree
add_index "events", ["created_at"], name: "index_events_on_created_at", using: :btree
add_index "events", ["project_id", "id"], name: "index_events_on_project_id_and_id", using: :btree
add_index "events", ["target_id"], name: "index_events_on_target_id", using: :btree
add_index "events", ["target_type"], name: "index_events_on_target_type", using: :btree
create_table "events_for_migration", force: :cascade do |t|
t.integer "project_id"
t.integer "author_id", null: false
t.integer "target_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "action", limit: 2, null: false
t.string "target_type"
end
add_index "events_for_migration", ["action"], name: "index_events_for_migration_on_action", using: :btree
add_index "events_for_migration", ["author_id"], name: "index_events_for_migration_on_author_id", using: :btree
add_index "events_for_migration", ["project_id", "id"], name: "index_events_for_migration_on_project_id_and_id", using: :btree
add_index "events_for_migration", ["target_type", "target_id"], name: "index_events_for_migration_on_target_type_and_target_id", using: :btree
add_index "events", ["action"], name: "index_events_on_action", using: :btree
add_index "events", ["author_id"], name: "index_events_on_author_id", using: :btree
add_index "events", ["project_id", "id"], name: "index_events_on_project_id_and_id", using: :btree
add_index "events", ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id", using: :btree
create_table "feature_gates", force: :cascade do |t|
t.string "feature_key", null: false
......@@ -594,8 +576,8 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree
create_table "gpg_keys", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "user_id"
t.binary "primary_keyid"
t.binary "fingerprint"
......@@ -607,8 +589,8 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_index "gpg_keys", ["user_id"], name: "index_gpg_keys_on_user_id", using: :btree
create_table "gpg_signatures", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "project_id"
t.integer "gpg_key_id"
t.binary "commit_sha"
......@@ -806,8 +788,8 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree
create_table "merge_request_diff_commits", id: false, force: :cascade do |t|
t.datetime "authored_date"
t.datetime "committed_date"
t.datetime_with_timezone "authored_date"
t.datetime_with_timezone "committed_date"
t.integer "merge_request_diff_id", null: false
t.integer "relative_order", null: false
t.binary "sha", null: false
......@@ -1229,9 +1211,8 @@ ActiveRecord::Schema.define(version: 20170905112933) do
t.integer "auto_cancel_pending_pipelines", default: 1, null: false
t.string "import_jid"
t.integer "cached_markdown_version"
t.datetime "last_repository_updated_at"
t.string "ci_config_path"
t.text "delete_error"
t.datetime "last_repository_updated_at"
t.integer "storage_version", limit: 2
t.boolean "resolve_outdated_diff_discussions"
end
......@@ -1627,6 +1608,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do
t.boolean "notified_of_own_activity"
t.string "preferred_language"
t.string "rss_token"
t.integer "theme_id", limit: 2
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
......@@ -1720,9 +1702,8 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade
add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade
add_foreign_key "environments", "projects", name: "fk_d1c8c1da6a", on_delete: :cascade
add_foreign_key "events", "projects", name: "fk_0434b48643", on_delete: :cascade
add_foreign_key "events_for_migration", "projects", on_delete: :cascade
add_foreign_key "events_for_migration", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade
add_foreign_key "events", "projects", on_delete: :cascade
add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade
add_foreign_key "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade
add_foreign_key "gpg_keys", "users", on_delete: :cascade
add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify
......@@ -1767,7 +1748,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_foreign_key "protected_tag_create_access_levels", "protected_tags", name: "fk_f7dfda8c51", on_delete: :cascade
add_foreign_key "protected_tag_create_access_levels", "users"
add_foreign_key "protected_tags", "projects", name: "fk_8e4af87648", on_delete: :cascade
add_foreign_key "push_event_payloads", "events_for_migration", column: "event_id", name: "fk_36c74129da", on_delete: :cascade
add_foreign_key "push_event_payloads", "events", name: "fk_36c74129da", on_delete: :cascade
add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade
add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade
add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade
......
......@@ -84,7 +84,7 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
- [Discussions](user/discussions/index.md) Threads, comments, and resolvable discussions in issues, commits, and merge requests.
- [Issues](user/project/issues/index.md)
- [Issue Board](user/project/issue_board.md)
- [Project issue Board](user/project/issue_board.md)
- [Issues and merge requests templates](user/project/description_templates.md): Create templates for submitting new issues and merge requests.
- [Labels](user/project/labels.md): Categorize your issues or merge requests based on descriptive titles.
- [Merge Requests](user/project/merge_requests/index.md)
......
......@@ -71,6 +71,15 @@ And in Markdown using fenced code blocks:
Alice -> Bob : Go Away
```
And in reStructuredText using a directive:
```
.. plantuml::
Bob -> Alice: hello
Alice -> Bob: Go Away
```
The above blocks will be converted to an HTML img tag with source pointing to the
PlantUML instance. If the PlantUML server is correctly configured, this should
render a nice diagram instead of the block:
......
......@@ -32,6 +32,7 @@ Parameters:
"twitter": "",
"website_url": "",
"email": "john@example.com",
"theme_id": 2,
"color_scheme_id": 1,
"projects_limit": 10,
"current_sign_in_at": null,
......
......@@ -28,12 +28,14 @@ Example response:
[
{
"id": 1,
"name": "user1",
"path": "user1",
"kind": "user",
"full_path": "user1"
},
{
"id": 2,
"name": "group1",
"path": "group1",
"kind": "group",
"full_path": "group1",
......@@ -42,6 +44,7 @@ Example response:
},
{
"id": 3,
"name": "bar",
"path": "bar",
"kind": "group",
"full_path": "foo/bar",
......@@ -77,6 +80,7 @@ Example response:
[
{
"id": 4,
"name": "twitter",
"path": "twitter",
"kind": "group",
"full_path": "twitter",
......
......@@ -39,6 +39,7 @@ Example response:
"twitter": "",
"website_url": "",
"email": "john@example.com",
"theme_id": 1,
"color_scheme_id": 1,
"projects_limit": 10,
"current_sign_in_at": "2015-07-07T07:10:58.392Z",
......
......@@ -72,6 +72,7 @@ GET /users
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
"theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
......@@ -105,6 +106,7 @@ GET /users
"organization": "",
"last_sign_in_at": null,
"confirmed_at": "2012-05-30T16:53:06.148Z",
"theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 3,
"projects_limit": 100,
......@@ -215,6 +217,7 @@ Parameters:
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
"theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
......@@ -341,6 +344,7 @@ GET /user
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
"theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
......@@ -387,6 +391,7 @@ GET /user
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
"theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
......
......@@ -96,7 +96,7 @@ services:
- tutum/wordpress:latest
```
If you don't [specify a service alias](#available-settings-for-services-entry),
If you don't [specify a service alias](#available-settings-for-services),
when the job is run, `tutum/wordpress` will be started and you will have
access to it from your build container under two hostnames to choose from:
......
......@@ -103,3 +103,24 @@ If that job fails, read the instructions in the job log for what to do next.
Contributors do not need to submit their changes to EE, GitLab Inc. employees
on the other hand need to make sure that their changes apply cleanly to both
CE and EE.
## Previewing the changes live
If you want to preview your changes live, you can use the manual `build-docs`
job in your merge request.
![Manual trigger a docs build](img/manual_build_docs.png)
This job will:
1. Create a new branch in the [gitlab-docs](https://gitlab.com/gitlab-com/gitlab-docs)
project named after the scheme: `<CE/EE-branch-slug>-built-from-ce-ee`
1. Trigger a pipeline and build the docs site with your changes
Look for the docs URL at the output of the `build-docs` job.
>**Note:**
Make sure that you always delete the branch of the merge request you were
working on. If you don't, the remote docs branch won't be removed either,
and the server where the Review Apps are hosted will eventually be out of
disk space.
......@@ -120,6 +120,11 @@ If a project is private, credentials will need to be provided for authorization.
The preferred way to do this, is by using [personal access tokens][pat].
The minimal scope needed is `read_registry`.
Example of using a personal access token:
```
docker login registry.example.com -u <your_username> -p <your_personal_access_token>
```
## Troubleshooting the GitLab Container Registry
### Basic Troubleshooting
......
......@@ -29,8 +29,9 @@ with all their related data and be moved into a new GitLab instance.
## Version history
| GitLab version | Import/Export version |
| -------- | -------- |
| 9.4.0 to current | 0.1.8 |
| ---------------- | --------------------- |
| 10.0 to current | 0.2.0 |
| 9.4.0 | 0.1.8 |
| 9.2.0 | 0.1.7 |
| 8.17.0 | 0.1.6 |
| 8.13.0 | 0.1.5 |
......
......@@ -4,7 +4,7 @@ A separate system for documentation called Wiki, is built right into each
GitLab project. It is enabled by default on all new projects and you can find
it under **Wiki** in your project.
Wikis are very convenient if you don't want to keep you documentation in your
Wikis are very convenient if you don't want to keep your documentation in your
repository, but you do want to keep it in the same project where your code
resides.
......
......@@ -37,7 +37,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
step 'I goto the Merge Requests page' do
page.within '.nav-sidebar' do
click_link "Merge Requests"
first(:link, "Merge Requests").click
end
end
......
......@@ -218,7 +218,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
# Wiki
step 'I go to wiki page' do
click_link "Wiki"
first(:link, "Wiki").click
expect(current_path).to eq project_wiki_path(@project, "home")
end
......
......@@ -11,7 +11,7 @@ module SharedActiveTab
end
def ensure_active_sub_tab(content)
expect(find('.sidebar-sub-level-items > li.active')).to have_content(content)
expect(find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)')).to have_content(content)
end
def ensure_active_sub_nav(content)
......@@ -23,7 +23,7 @@ module SharedActiveTab
end
step 'no other sub tabs should be active' do
expect(page).to have_selector('.sidebar-sub-level-items > li.active', count: 1)
expect(page).to have_selector('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)', count: 1)
end
step 'no other sub navs should be active' do
......
......@@ -17,14 +17,9 @@ class Spinach::Features::User < Spinach::FeatureSteps
Issues::CreateService.new(project, user, issue_params).execute
# Push code contribution
push_params = {
project: project,
action: Event::PUSHED,
author_id: user.id,
data: { commit_count: 3 }
}
Event.create(push_params)
event = create(:push_event, project: project, author: user)
create(:push_event_payload, event: event, commit_count: 3)
end
step 'I should see contributed projects' do
......@@ -38,6 +33,6 @@ class Spinach::Features::User < Spinach::FeatureSteps
end
def contributed_project
@contributed_project ||= create(:project, :public)
@contributed_project ||= create(:project, :public, :empty_repo)
end
end
......@@ -45,7 +45,7 @@ module API
expose :confirmed_at
expose :last_activity_on
expose :email
expose :color_scheme_id, :projects_limit, :current_sign_in_at
expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at
expose :identities, using: Entities::Identity
expose :can_create_group?, as: :can_create_group
expose :can_create_project?, as: :can_create_project
......@@ -557,7 +557,7 @@ module API
end
class Event < Grape::Entity
expose :title, :project_id, :action_name
expose :project_id, :action_name
expose :target_id, :target_iid, :target_type, :author_id
expose :target_title
expose :created_at
......
......@@ -31,7 +31,7 @@ module API
end
class Event < Grape::Entity
expose :title, :project_id, :action_name
expose :project_id, :action_name
expose :target_id, :target_type, :author_id
expose :target_title
expose :created_at
......
......@@ -3,7 +3,7 @@ module Gitlab
extend self
# For every version update, the version history in import_export.md has to be kept up to date.
VERSION = '0.1.8'.freeze
VERSION = '0.2.0'.freeze
FILENAME_LIMIT = 50
def export_path(relative_path:)
......
......@@ -70,7 +70,6 @@ module Gitlab
reset_tokens!
remove_encrypted_attributes!
@relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data']
set_st_diff_commits if @relation_name == :merge_request_diff
set_diff if @relation_name == :merge_request_diff_files
end
......
......@@ -26,6 +26,7 @@ module Gitlab
apple-touch-icon.png
assets
autocomplete
boards
ci
dashboard
deploy.html
......
module Gitlab
# Module containing GitLab's application theme definitions and helper methods
# for accessing them.
module Themes
extend self
# Theme ID used when no `default_theme` configuration setting is provided.
APPLICATION_DEFAULT = 1
# Struct class representing a single Theme
Theme = Struct.new(:id, :name, :css_class)
# All available Themes
THEMES = [
Theme.new(1, 'Indigo', 'ui_indigo'),
Theme.new(2, 'Dark', 'ui_dark'),
Theme.new(3, 'Light', 'ui_light'),
Theme.new(4, 'Blue', 'ui_blue'),
Theme.new(5, 'Green', 'ui_green')
].freeze
# Convenience method to get a space-separated String of all the theme
# classes that might be applied to the `body` element
#
# Returns a String
def body_classes
THEMES.collect(&:css_class).uniq.join(' ')
end
# Get a Theme by its ID
#
# If the ID is invalid, returns the default Theme.
#
# id - Integer ID
#
# Returns a Theme
def by_id(id)
THEMES.detect { |t| t.id == id } || default
end
# Returns the number of defined Themes
def count
THEMES.size
end
# Get the default Theme
#
# Returns a Theme
def default
by_id(default_id)
end
# Iterate through each Theme
#
# Yields the Theme object
def each(&block)
THEMES.each(&block)
end
# Get the Theme for the specified user, or the default
#
# user - User record
#
# Returns a Theme
def for_user(user)
if user
by_id(user.theme_id)
else
default
end
end
private
def default_id
@default_id ||= begin
id = Gitlab.config.gitlab.default_theme.to_i
theme_ids = THEMES.map(&:id)
theme_ids.include?(id) ? id : APPLICATION_DEFAULT
end
end
end
end
......@@ -9,5 +9,16 @@ namespace :gitlab do
task data: :environment do
puts YAML.load_file(Gitlab::ImportExport.config_file)['project_tree'].to_yaml(SortKeys: true)
end
desc 'GitLab | Bumps the Import/Export version for test_project_export.tar.gz'
task bump_test_version: :environment do
Dir.mktmpdir do |tmp_dir|
system("tar -zxf spec/features/projects/import_export/test_project_export.tar.gz -C #{tmp_dir} > /dev/null")
File.write(File.join(tmp_dir, 'VERSION'), Gitlab::ImportExport.version, mode: 'w')
system("tar -zcvf spec/features/projects/import_export/test_project_export.tar.gz -C #{tmp_dir} . > /dev/null")
end
puts "Updated to #{Gitlab::ImportExport.version}"
end
end
end
#!/usr/bin/env ruby
require 'gitlab'
#
# Give the remote branch a different name than the current one
# in order to avoid conflicts
#
@docs_branch = "#{ENV["CI_COMMIT_REF_SLUG"]}-built-from-ce-ee"
GITLAB_DOCS_REPO = 'gitlab-com/gitlab-docs'
#
# Configure credentials to be used with gitlab gem
#
Gitlab.configure do |config|
config.endpoint = 'https://gitlab.com/api/v4'
config.private_token = ENV["DOCS_API_TOKEN"] # GitLab Docs bot access token which has only Developer access to gitlab-docs
end
#
# Dummy way to find out in which repo we are, CE or EE
#
def is_ee?
File.exists?('CHANGELOG-EE.md')
end
#
# Create a remote branch in gitlab-docs
#
def create_remote_branch
Gitlab.create_branch(GITLAB_DOCS_REPO, @docs_branch, 'master')
puts "Remote branch '#{@docs_branch}' created"
rescue Gitlab::Error::BadRequest => e
puts "Remote branch '#{@docs_branch}' already exists"
end
#
# Remove a remote branch in gitlab-docs
#
def remove_remote_branch
Gitlab.delete_branch(GITLAB_DOCS_REPO, @docs_branch)
puts "Remote branch '#{@docs_branch}' deleted"
end
#
# Trigger a pipeline in gitlab-docs
#
def trigger_pipeline
# Overriding vars in https://gitlab.com/gitlab-com/gitlab-docs/blob/master/.gitlab-ci.yml
param_name = is_ee? ? 'BRANCH_EE' : 'BRANCH_CE'
# The review app URL
app_url = "http://#{@docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{is_ee? ? 'ee' : 'ce'}"
# Create the pipeline
puts "=> Triggering a pipeline..."
pipeline = Gitlab.run_trigger(GITLAB_DOCS_REPO, ENV["DOCS_TRIGGER_TOKEN"], @docs_branch, { param_name => ENV["CI_COMMIT_REF_NAME"] })
puts "=> Pipeline created:"
puts ""
puts "https://gitlab.com/gitlab-com/gitlab-docs/pipelines/#{pipeline.id}"
puts ""
puts "=> Preview your changes live at:"
puts ""
puts app_url
puts ""
end
#
# When the first argument is deploy then create the branch and trigger pipeline
# When it is 'stop', it deleted the remote branch. That way, we ensure there
# are no stale remote branches and the Review server doesn't fill.
#
case ARGV[0]
when 'deploy'
create_remote_branch
trigger_pipeline
when 'cleanup'
remove_remote_branch
end
require 'spec_helper'
describe Projects::Boards::IssuesController do
describe Boards::IssuesController do
let(:project) { create(:project) }
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
......@@ -133,6 +133,22 @@ describe Projects::Boards::IssuesController do
expect(response).to have_http_status(404)
end
end
context 'with invalid board id' do
it 'returns a not found 404 response' do
create_issue user: user, board: 999, list: list1, title: 'New issue'
expect(response).to have_http_status(404)
end
end
context 'with invalid list id' do
it 'returns a not found 404 response' do
create_issue user: user, board: board, list: 999, title: 'New issue'
expect(response).to have_http_status(404)
end
end
end
context 'with unauthorized user' do
......@@ -146,17 +162,15 @@ describe Projects::Boards::IssuesController do
def create_issue(user:, board:, list:, title:)
sign_in(user)
post :create, namespace_id: project.namespace.to_param,
project_id: project,
board_id: board.to_param,
post :create, board_id: board.to_param,
list_id: list.to_param,
issue: { title: title },
issue: { title: title, project_id: project.id },
format: :json
end
end
describe 'PATCH update' do
let(:issue) { create(:labeled_issue, project: project, labels: [planning]) }
let!(:issue) { create(:labeled_issue, project: project, labels: [planning]) }
context 'with valid params' do
it 'returns a successful 200 response' do
......@@ -186,7 +200,7 @@ describe Projects::Boards::IssuesController do
end
it 'returns a not found 404 response for invalid issue id' do
move user: user, board: board, issue: 999, from_list_id: list1.id, to_list_id: list2.id
move user: user, board: board, issue: double(id: 999), from_list_id: list1.id, to_list_id: list2.id
expect(response).to have_http_status(404)
end
......@@ -210,9 +224,9 @@ describe Projects::Boards::IssuesController do
sign_in(user)
patch :update, namespace_id: project.namespace.to_param,
project_id: project,
project_id: project.id,
board_id: board.to_param,
id: issue.to_param,
id: issue.id,
from_list_id: from_list_id,
to_list_id: to_list_id,
format: :json
......
require 'spec_helper'
describe Projects::Boards::ListsController do
describe Boards::ListsController do
let(:project) { create(:project) }
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
......
......@@ -25,7 +25,8 @@ describe Profiles::PreferencesController do
def go(params: {}, format: :js)
params.reverse_merge!(
color_scheme_id: '1',
dashboard: 'stars'
dashboard: 'stars',
theme_id: '1'
)
patch :update, user: params, format: format
......@@ -40,7 +41,8 @@ describe Profiles::PreferencesController do
it "changes the user's preferences" do
prefs = {
color_scheme_id: '1',
dashboard: 'stars'
dashboard: 'stars',
theme_id: '2'
}.with_indifferent_access
expect(user).to receive(:assign_attributes).with(prefs)
......
......@@ -7,6 +7,7 @@ FactoryGirl.define do
group nil
project_id nil
group_id nil
parent nil
end
trait :active do
......@@ -26,6 +27,9 @@ FactoryGirl.define do
milestone.project = evaluator.project
elsif evaluator.project_id
milestone.project_id = evaluator.project_id
elsif evaluator.parent
id = evaluator.parent.id
evaluator.parent.is_a?(Group) ? board.group_id = id : evaluator.project_id = id
else
milestone.project = create(:project)
end
......
......@@ -14,8 +14,8 @@ RSpec.describe 'admin active tab' do
shared_examples 'page has active sub tab' do |title|
it "activates #{title} sub tab" do
expect(page).to have_selector('.sidebar-sub-level-items li.active', count: 1)
expect(page.find('.sidebar-sub-level-items li.active')).to have_content(title)
expect(page).to have_selector('.sidebar-sub-level-items > li.active', count: 2)
expect(page.all('.sidebar-sub-level-items > li.active')[1]).to have_content(title)
end
end
......
......@@ -48,7 +48,7 @@ feature 'Admin updates settings' do
end
scenario 'Change Slack Notifications Service template settings' do
click_link 'Service Templates'
first(:link, 'Service Templates').click
click_link 'Slack notifications'
fill_in 'Webhook', with: 'http://localhost'
fill_in 'Username', with: 'test_user'
......
......@@ -84,25 +84,11 @@ feature 'Dashboard Projects' do
end
context 'last push widget' do
let(:push_event_data) do
{
before: Gitlab::Git::BLANK_SHA,
after: '0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e',
ref: 'refs/heads/feature',
user_id: user.id,
user_name: user.name,
repository: {
name: project.name,
url: 'localhost/rubinius',
description: '',
homepage: 'localhost/rubinius',
private: true
}
}
end
let!(:push_event) { create(:event, :pushed, data: push_event_data, project: project, author: user) }
before do
event = create(:push_event, project: project, author: user)
create(:push_event_payload, event: event, ref: 'feature', action: :created)
visit dashboard_projects_path
end
......
......@@ -51,7 +51,7 @@ feature 'Groups > Members > Request access' do
expect(group.requesters.exists?(user_id: user)).to be_truthy
click_link 'Members'
first(:link, 'Members').click
page.within('.content') do
expect(page).not_to have_content(user.name)
......
......@@ -8,10 +8,15 @@
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"project_id": { "type": ["integer", "null"] },
"title": { "type": "string" },
"confidential": { "type": "boolean" },
"due_date": { "type": ["date", "null"] },
"relative_position": { "type": "integer" },
"project": {
"id": { "type": "integer" },
"path": { "type": "string" }
},
"labels": {
"type": "array",
"items": {
......@@ -34,6 +39,7 @@
"type": "string",
"pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
},
"type": { "type": "string" },
"title": { "type": "string" },
"priority": { "type": ["integer", "null"] }
},
......
......@@ -19,6 +19,7 @@
"organization",
"last_sign_in_at",
"confirmed_at",
"theme_id",
"color_scheme_id",
"projects_limit",
"current_sign_in_at",
......
require 'spec_helper'
describe PreferencesHelper do
describe 'dashboard_choices' do
describe '#dashboard_choices' do
it 'raises an exception when defined choices may be missing' do
expect(User).to receive(:dashboards).and_return(foo: 'foo')
expect { helper.dashboard_choices }.to raise_error(RuntimeError)
......@@ -26,7 +26,33 @@ describe PreferencesHelper do
end
end
describe 'user_color_scheme' do
describe '#user_application_theme' do
context 'with a user' do
it "returns user's theme's css_class" do
stub_user(theme_id: 3)
expect(helper.user_application_theme).to eq 'ui_light'
end
it 'returns the default when id is invalid' do
stub_user(theme_id: Gitlab::Themes.count + 5)
allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(1)
expect(helper.user_application_theme).to eq 'ui_indigo'
end
end
context 'without a user' do
it 'returns the default theme' do
stub_user
expect(helper.user_application_theme).to eq Gitlab::Themes.default.css_class
end
end
end
describe '#user_color_scheme' do
context 'with a user' do
it "returns user's scheme's css_class" do
allow(helper).to receive(:current_user)
......
/* global BoardService */
/* global mockBoardService */
import Vue from 'vue';
import '~/boards/stores/boards_store';
import boardBlankState from '~/boards/components/board_blank_state';
......@@ -12,7 +13,7 @@ describe('Boards blank state', () => {
const Comp = Vue.extend(boardBlankState);
gl.issueBoards.BoardsStore.create();
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.boardService = mockBoardService();
spyOn(gl.boardService, 'generateDefaultLists').and.callFake(() => new Promise((resolve, reject) => {
if (fail) {
......
......@@ -4,6 +4,7 @@
/* global listObj */
/* global boardsMockInterceptor */
/* global BoardService */
/* global mockBoardService */
import Vue from 'vue';
import '~/boards/models/assignee';
......@@ -14,13 +15,13 @@ import '~/boards/stores/boards_store';
import boardCard from '~/boards/components/board_card';
import './mock_data';
describe('Issue card', () => {
describe('Board card', () => {
let vm;
beforeEach((done) => {
Vue.http.interceptors.push(boardsMockInterceptor);
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.boardService = mockBoardService();
gl.issueBoards.BoardsStore.create();
gl.issueBoards.BoardsStore.detail.issue = {};
......
......@@ -3,6 +3,7 @@
/* global List */
/* global listObj */
/* global ListIssue */
/* global mockBoardService */
import Vue from 'vue';
import _ from 'underscore';
import Sortable from 'vendor/Sortable';
......@@ -24,7 +25,7 @@ describe('Board list component', () => {
document.body.appendChild(el);
Vue.http.interceptors.push(boardsMockInterceptor);
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.boardService = mockBoardService();
gl.issueBoards.BoardsStore.create();
gl.IssueBoardsApp = new Vue();
......@@ -32,6 +33,7 @@ describe('Board list component', () => {
const list = new List(listObj);
const issue = new ListIssue({
title: 'Testing',
id: 1,
iid: 1,
confidential: false,
labels: [],
......
......@@ -2,6 +2,7 @@
/* global BoardService */
/* global List */
/* global listObj */
/* global mockBoardService */
import Vue from 'vue';
import boardNewIssue from '~/boards/components/board_new_issue';
......@@ -35,7 +36,7 @@ describe('Issue boards new issue form', () => {
const BoardNewIssueComp = Vue.extend(boardNewIssue);
Vue.http.interceptors.push(boardsMockInterceptor);
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.boardService = mockBoardService();
gl.issueBoards.BoardsStore.create();
gl.IssueBoardsApp = new Vue();
......
......@@ -4,6 +4,7 @@
/* global listObj */
/* global listObjDuplicate */
/* global ListIssue */
/* global mockBoardService */
import Vue from 'vue';
import Cookies from 'js-cookie';
......@@ -20,7 +21,7 @@ import './mock_data';
describe('Store', () => {
beforeEach(() => {
Vue.http.interceptors.push(boardsMockInterceptor);
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.boardService = mockBoardService();
gl.issueBoards.BoardsStore.create();
spyOn(gl.boardService, 'moveIssue').and.callFake(() => new Promise((resolve) => {
......@@ -78,7 +79,7 @@ describe('Store', () => {
it('persists new list', (done) => {
gl.issueBoards.BoardsStore.new({
title: 'Test',
type: 'label',
list_type: 'label',
label: {
id: 1,
title: 'Testing',
......@@ -210,6 +211,7 @@ describe('Store', () => {
it('moves issue in list', (done) => {
const issue = new ListIssue({
title: 'Testing',
id: 2,
iid: 2,
confidential: false,
labels: [],
......
/* global mockBoardService */
import Vue from 'vue';
import '~/boards/services/board_service';
import '~/boards/components/board';
import '~/boards/models/list';
import '../mock_data';
describe('Board component', () => {
let vm;
......@@ -13,8 +15,12 @@ describe('Board component', () => {
el = document.createElement('div');
document.body.appendChild(el);
// eslint-disable-next-line no-undef
gl.boardService = new BoardService('/', '/', 1);
gl.boardService = mockBoardService({
boardsEndpoint: '/',
listsEndpoint: '/',
bulkUpdatePath: '/',
boardId: 1,
});
vm = new gl.issueBoards.Board({
propsData: {
......
......@@ -37,6 +37,7 @@ describe('Issue card component', () => {
list = listObj;
issue = new ListIssue({
title: 'Testing',
id: 1,
iid: 1,
confidential: false,
labels: [list.label],
......@@ -238,7 +239,6 @@ describe('Issue card component', () => {
});
describe('labels', () => {
describe('exists', () => {
beforeEach((done) => {
component.issue.addLabel(label1);
......@@ -298,5 +298,4 @@ describe('Issue card component', () => {
.catch(done.fail);
});
});
});
});
/* eslint-disable comma-dangle */
/* global BoardService */
/* global ListIssue */
/* global mockBoardService */
import Vue from 'vue';
import '~/lib/utils/url_utility';
......@@ -16,11 +17,12 @@ describe('Issue model', () => {
let issue;
beforeEach(() => {
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.boardService = mockBoardService();
gl.issueBoards.BoardsStore.create();
issue = new ListIssue({
title: 'Testing',
id: 1,
iid: 1,
confidential: false,
labels: [{
......
/* eslint-disable comma-dangle */
/* global boardsMockInterceptor */
/* global BoardService */
/* global mockBoardService */
/* global List */
/* global ListIssue */
/* global listObj */
......@@ -22,7 +23,9 @@ describe('List model', () => {
beforeEach(() => {
Vue.http.interceptors.push(boardsMockInterceptor);
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.boardService = mockBoardService({
bulkUpdatePath: '/test/issue-boards/board/1/lists',
});
gl.issueBoards.BoardsStore.create();
list = new List(listObj);
......@@ -92,6 +95,7 @@ describe('List model', () => {
const listDup = new List(listObjDuplicate);
const issue = new ListIssue({
title: 'Testing',
id: _.random(10000),
iid: _.random(10000),
confidential: false,
labels: [list.label, listDup.label],
......@@ -118,6 +122,7 @@ describe('List model', () => {
for (let i = 0; i < 30; i += 1) {
list.issues.push(new ListIssue({
title: 'Testing',
id: _.random(10000) + i,
iid: _.random(10000) + i,
confidential: false,
labels: [list.label],
......@@ -137,7 +142,7 @@ describe('List model', () => {
it('does not increase page number if issue count is less than the page size', () => {
list.issues.push(new ListIssue({
title: 'Testing',
iid: _.random(10000),
id: _.random(10000),
confidential: false,
labels: [list.label],
assignees: [],
......@@ -156,7 +161,7 @@ describe('List model', () => {
spyOn(gl.boardService, 'newIssue').and.returnValue(Promise.resolve({
json() {
return {
iid: 42,
id: 42,
};
},
}));
......@@ -165,14 +170,14 @@ describe('List model', () => {
it('adds new issue to top of list', (done) => {
list.issues.push(new ListIssue({
title: 'Testing',
iid: _.random(10000),
id: _.random(10000),
confidential: false,
labels: [list.label],
assignees: [],
}));
const dummyIssue = new ListIssue({
title: 'new issue',
iid: _.random(10000),
id: _.random(10000),
confidential: false,
labels: [list.label],
assignees: [],
......
/* global BoardService */
/* eslint-disable comma-dangle, no-unused-vars, quote-props */
const listObj = {
......@@ -28,19 +29,19 @@ const listObjDuplicate = {
const BoardsMockData = {
'GET': {
'/test/issue-boards/board/1/lists{/id}/issues': {
'/test/boards/1{/id}/issues': {
issues: [{
title: 'Testing',
id: 1,
iid: 1,
confidential: false,
labels: [],
assignees: [],
}],
size: 1
}
},
'POST': {
'/test/issue-boards/board/1/lists{/id}': listObj
'/test/boards/1{/id}': listObj
},
'PUT': {
'/test/issue-boards/board/1/lists{/id}': {}
......@@ -58,7 +59,22 @@ const boardsMockInterceptor = (request, next) => {
}));
};
const mockBoardService = (opts = {}) => {
const boardsEndpoint = opts.boardsEndpoint || '/test/issue-boards/board';
const listsEndpoint = opts.listsEndpoint || '/test/boards/1';
const bulkUpdatePath = opts.bulkUpdatePath || '';
const boardId = opts.boardId || '1';
return new BoardService({
boardsEndpoint,
listsEndpoint,
bulkUpdatePath,
boardId,
});
};
window.listObj = listObj;
window.listObjDuplicate = listObjDuplicate;
window.BoardsMockData = BoardsMockData;
window.boardsMockInterceptor = boardsMockInterceptor;
window.mockBoardService = mockBoardService;
......@@ -18,6 +18,7 @@ describe('Modal store', () => {
issue = new ListIssue({
title: 'Testing',
id: 1,
iid: 1,
confidential: false,
labels: [],
......@@ -25,6 +26,7 @@ describe('Modal store', () => {
});
issue2 = new ListIssue({
title: 'Testing',
id: 1,
iid: 2,
confidential: false,
labels: [],
......
......@@ -34,6 +34,8 @@ describe('Fly out sidebar navigation', () => {
document.body.innerHTML = '';
breakpointSize = 'lg';
mousePos.length = 0;
setSidebar(null);
});
describe('calculateTop', () => {
......@@ -242,6 +244,32 @@ describe('Fly out sidebar navigation', () => {
).toBe('block');
});
it('shows collapsed only sub-items if icon only sidebar', () => {
const subItems = el.querySelector('.sidebar-sub-level-items');
const sidebar = document.createElement('div');
sidebar.classList.add('sidebar-icons-only');
subItems.classList.add('is-fly-out-only');
setSidebar(sidebar);
showSubLevelItems(el);
expect(
el.querySelector('.sidebar-sub-level-items').style.display,
).toBe('block');
});
it('does not show collapsed only sub-items if icon only sidebar', () => {
const subItems = el.querySelector('.sidebar-sub-level-items');
subItems.classList.add('is-fly-out-only');
showSubLevelItems(el);
expect(
subItems.style.display,
).not.toBe('block');
});
it('sets transform of sub-items', () => {
const subItems = el.querySelector('.sidebar-sub-level-items');
showSubLevelItems(el);
......@@ -283,10 +311,6 @@ describe('Fly out sidebar navigation', () => {
});
describe('canShowActiveSubItems', () => {
afterEach(() => {
setSidebar(null);
});
it('returns true by default', () => {
expect(
canShowActiveSubItems(el),
......
......@@ -118,7 +118,7 @@ describe('Issue', function() {
this.$triggeredButton = $btn;
this.$projectIssuesCounter = $('.issue_counter');
this.$projectIssuesCounter = $('.issue_counter').first();
this.$projectIssuesCounter.text('1,001');
this.issueStateDeferred = new jQuery.Deferred();
......
require 'spec_helper'
describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads::Event do
describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads::Event, :migration, schema: 20170608152748 do
describe '#commit_title' do
it 'returns nil when there are no commits' do
expect(described_class.new.commit_title).to be_nil
......@@ -215,9 +215,17 @@ end
# to a specific version of the database where said table is still present.
#
describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migration, schema: 20170825154015 do
let(:user_class) do
Class.new(ActiveRecord::Base) do
self.table_name = 'users'
end
end
let(:migration) { described_class.new }
let(:project) { create(:project_empty_repo) }
let(:author) { create(:user) }
let(:user_class) { table(:users) }
let(:author) { build(:user).becomes(user_class).tap(&:save!).becomes(User) }
let(:namespace) { create(:namespace, owner: author) }
let(:project) { create(:project_empty_repo, namespace: namespace, creator: author) }
# We can not rely on FactoryGirl as the state of Event may change in ways that
# the background migration does not expect, hence we use the Event class of
......
......@@ -75,8 +75,6 @@
"id": 487,
"target_type": "Milestone",
"target_id": 1,
"title": null,
"data": null,
"project_id": 46,
"created_at": "2016-06-14T15:02:04.418Z",
"updated_at": "2016-06-14T15:02:04.418Z",
......@@ -364,8 +362,6 @@
"id": 487,
"target_type": "Milestone",
"target_id": 1,
"title": null,
"data": null,
"project_id": 46,
"created_at": "2016-06-14T15:02:04.418Z",
"updated_at": "2016-06-14T15:02:04.418Z",
......@@ -2311,8 +2307,6 @@
"id": 487,
"target_type": "Milestone",
"target_id": 1,
"title": null,
"data": null,
"project_id": 46,
"created_at": "2016-06-14T15:02:04.418Z",
"updated_at": "2016-06-14T15:02:04.418Z",
......@@ -2336,8 +2330,6 @@
"id": 240,
"target_type": "Milestone",
"target_id": 20,
"title": null,
"data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:04.593Z",
"updated_at": "2016-06-14T15:02:04.593Z",
......@@ -2348,8 +2340,6 @@
"id": 60,
"target_type": "Milestone",
"target_id": 20,
"title": null,
"data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:04.593Z",
"updated_at": "2016-06-14T15:02:04.593Z",
......@@ -2373,8 +2363,6 @@
"id": 241,
"target_type": "Milestone",
"target_id": 19,
"title": null,
"data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:04.585Z",
"updated_at": "2016-06-14T15:02:04.585Z",
......@@ -2385,41 +2373,6 @@
"id": 59,
"target_type": "Milestone",
"target_id": 19,
"title": null,
"data": {
"object_kind": "push",
"before": "0000000000000000000000000000000000000000",
"after": "de990aa15829d0ab182ad5a55b4c527846c0d39c",
"ref": "refs/heads/removable-group-owner",
"checkout_sha": "de990aa15829d0ab182ad5a55b4c527846c0d39c",
"message": null,
"user_id": 273486,
"user_name": "James Lopez",
"user_email": "james@jameslopez.es",
"project_id": 562317,
"repository": {
"name": "GitLab Community Edition",
"url": "git@gitlab.com:james11/gitlab-ce.git",
"description": "Version Control on your Server. See http://gitlab.org/gitlab-ce/ and the README for more information",
"homepage": "https://gitlab.com/james11/gitlab-ce",
"git_http_url": "https://gitlab.com/james11/gitlab-ce.git",
"git_ssh_url": "git@gitlab.com:james11/gitlab-ce.git",
"visibility_level": 20
},
"commits": [
{
"id": "de990aa15829d0ab182ad5a55b4c527846c0d39c",
"message": "fixed last group owner issue and added test\\n",
"timestamp": "2015-10-29T16:10:27+00:00",
"url": "https://gitlab.com/james11/gitlab-ce/commit/de990aa15829d0ab182ad5a55b4c527846c0d39c",
"author": {
"name": "James Lopez",
"email": "james.lopez@vodafone.com"
}
}
],
"total_commits_count": 1
},
"project_id": 5,
"created_at": "2016-06-14T15:02:04.585Z",
"updated_at": "2016-06-14T15:02:04.585Z",
......@@ -2947,8 +2900,6 @@
"id": 221,
"target_type": "MergeRequest",
"target_id": 27,
"title": null,
"data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:36.703Z",
"updated_at": "2016-06-14T15:02:36.703Z",
......@@ -2959,8 +2910,6 @@
"id": 187,
"target_type": "MergeRequest",
"target_id": 27,
"title": null,
"data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:36.703Z",
"updated_at": "2016-06-14T15:02:36.703Z",
......@@ -3230,8 +3179,6 @@
"id": 222,
"target_type": "MergeRequest",
"target_id": 26,
"title": null,
"data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:36.496Z",
"updated_at": "2016-06-14T15:02:36.496Z",
......@@ -3242,8 +3189,6 @@
"id": 186,
"target_type": "MergeRequest",
"target_id": 26,
"title": null,
"data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:36.496Z",
"updated_at": "2016-06-14T15:02:36.496Z",
......@@ -3513,8 +3458,6 @@
"id": 223,
"target_type": "MergeRequest",
"target_id": 15,
"title": null,
"data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:25.262Z",
"updated_at": "2016-06-14T15:02:25.262Z",
......@@ -3525,8 +3468,6 @@
"id": 175,
"target_type": "MergeRequest",
"target_id": 15,
"title": null,
"data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:25.262Z",
"updated_at": "2016-06-14T15:02:25.262Z",
......@@ -4202,8 +4143,6 @@
"id": 224,
"target_type": "MergeRequest",
"target_id": 14,
"title": null,
"data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:25.113Z",
"updated_at": "2016-06-14T15:02:25.113Z",
......@@ -4214,8 +4153,6 @@
"id": 174,
"target_type": "MergeRequest",
"target_id": 14,
"title": null,
"data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:25.113Z",
"updated_at": "2016-06-14T15:02:25.113Z",
......@@ -4274,9 +4211,7 @@
{
"id": 529,
"target_type": "Note",
"target_id": 2521,
"title": "test levels",
"data": null,
"target_id": 793,
"project_id": 4,
"created_at": "2016-07-07T14:35:12.128Z",
"updated_at": "2016-07-07T14:35:12.128Z",
......@@ -4749,8 +4684,6 @@
"id": 225,
"target_type": "MergeRequest",
"target_id": 13,
"title": null,
"data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:24.636Z",
"updated_at": "2016-06-14T15:02:24.636Z",
......@@ -4761,8 +4694,6 @@
"id": 173,
"target_type": "MergeRequest",
"target_id": 13,
"title": null,
"data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:24.636Z",
"updated_at": "2016-06-14T15:02:24.636Z",
......@@ -5247,8 +5178,6 @@
"id": 226,
"target_type": "MergeRequest",
"target_id": 12,
"title": null,
"data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:24.253Z",
"updated_at": "2016-06-14T15:02:24.253Z",
......@@ -5259,8 +5188,6 @@
"id": 172,
"target_type": "MergeRequest",
"target_id": 12,
"title": null,
"data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:24.253Z",
"updated_at": "2016-06-14T15:02:24.253Z",
......@@ -5506,8 +5433,6 @@
"id": 227,
"target_type": "MergeRequest",
"target_id": 11,
"title": null,
"data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:23.865Z",
"updated_at": "2016-06-14T15:02:23.865Z",
......@@ -5518,8 +5443,6 @@
"id": 171,
"target_type": "MergeRequest",
"target_id": 11,
"title": null,
"data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:23.865Z",
"updated_at": "2016-06-14T15:02:23.865Z",
......@@ -6195,8 +6118,6 @@
"id": 228,
"target_type": "MergeRequest",
"target_id": 10,
"title": null,
"data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:23.660Z",
"updated_at": "2016-06-14T15:02:23.660Z",
......@@ -6207,8 +6128,6 @@
"id": 170,
"target_type": "MergeRequest",
"target_id": 10,
"title": null,
"data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:23.660Z",
"updated_at": "2016-06-14T15:02:23.660Z",
......@@ -6478,8 +6397,6 @@
"id": 229,
"target_type": "MergeRequest",
"target_id": 9,
"title": null,
"data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:22.927Z",
"updated_at": "2016-06-14T15:02:22.927Z",
......@@ -6490,8 +6407,6 @@
"id": 169,
"target_type": "MergeRequest",
"target_id": 9,
"title": null,
"data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:22.927Z",
"updated_at": "2016-06-14T15:02:22.927Z",
......
......@@ -57,10 +57,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(Ci::Pipeline.where(ref: nil)).not_to be_empty
end
it 'restores the correct event with symbolised data' do
expect(Event.where.not(data: nil).first.data[:ref]).not_to be_empty
end
it 'preserves updated_at on issues' do
issue = Issue.where(description: 'Aliquam enim illo et possimus.').first
......@@ -80,7 +76,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
context 'event at forth level of the tree' do
let(:event) { Event.where(title: 'test levels').first }
let(:event) { Event.where(action: 6).first }
it 'restores the event' do
expect(event).not_to be_nil
......
......@@ -29,8 +29,6 @@ Event:
- id
- target_type
- target_id
- title
- data
- project_id
- created_at
- updated_at
......
require 'spec_helper'
describe Gitlab::Themes, lib: true do
describe '.body_classes' do
it 'returns a space-separated list of class names' do
css = described_class.body_classes
expect(css).to include('ui_indigo')
expect(css).to include(' ui_dark ')
expect(css).to include(' ui_blue')
end
end
describe '.by_id' do
it 'returns a Theme by its ID' do
expect(described_class.by_id(1).name).to eq 'Indigo'
expect(described_class.by_id(3).name).to eq 'Light'
end
end
describe '.default' do
it 'returns the default application theme' do
allow(described_class).to receive(:default_id).and_return(2)
expect(described_class.default.id).to eq 2
end
it 'prevents an infinite loop when configuration default is invalid' do
default = described_class::APPLICATION_DEFAULT
themes = described_class::THEMES
config = double(default_theme: 0).as_null_object
allow(Gitlab).to receive(:config).and_return(config)
expect(described_class.default.id).to eq default
config = double(default_theme: themes.size + 5).as_null_object
allow(Gitlab).to receive(:config).and_return(config)
expect(described_class.default.id).to eq default
end
end
describe '.each' do
it 'passes the block to the THEMES Array' do
ids = []
described_class.each { |theme| ids << theme.id }
expect(ids).not_to be_empty
end
end
end
......@@ -2,6 +2,8 @@ require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170607121233_convert_custom_notification_settings_to_columns')
describe ConvertCustomNotificationSettingsToColumns, :migration do
let(:user_class) { table(:users) }
let(:settings_params) do
[
{ level: 0, events: [:new_note] }, # disabled, single event
......@@ -19,7 +21,7 @@ describe ConvertCustomNotificationSettingsToColumns, :migration do
events[event] = true
end
user = create(:user)
user = build(:user).becomes(user_class).tap(&:save!)
create_params = { user_id: user.id, level: params[:level], events: events }
notification_setting = described_class::NotificationSetting.create(create_params)
......@@ -35,7 +37,7 @@ describe ConvertCustomNotificationSettingsToColumns, :migration do
events[event] = true
end
user = create(:user)
user = build(:user).becomes(user_class).tap(&:save!)
create_params = events.merge(user_id: user.id, level: params[:level])
notification_setting = described_class::NotificationSetting.create(create_params)
......
......@@ -11,7 +11,6 @@ describe Event do
it { is_expected.to respond_to(:author_email) }
it { is_expected.to respond_to(:issue_title) }
it { is_expected.to respond_to(:merge_request_title) }
it { is_expected.to respond_to(:commits) }
end
describe 'Callbacks' do
......
......@@ -716,6 +716,7 @@ describe User do
it "applies defaults to user" do
expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit)
expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group)
expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme)
expect(user.external).to be_falsey
end
end
......@@ -726,6 +727,7 @@ describe User do
it "applies defaults to user" do
expect(user.projects_limit).to eq(123)
expect(user.can_create_group).to be_falsey
expect(user.theme_id).to eq(1)
end
end
......
......@@ -8,7 +8,7 @@ describe Boards::Issues::CreateService do
let(:label) { create(:label, project: project, name: 'in-progress') }
let!(:list) { create(:list, board: board, label: label, position: 0) }
subject(:service) { described_class.new(project, user, board_id: board.id, list_id: list.id, title: 'New issue') }
subject(:service) { described_class.new(board.parent, project, user, board_id: board.id, list_id: list.id, title: 'New issue') }
before do
project.team << [user, :developer]
......
......@@ -98,7 +98,7 @@ describe Boards::Issues::MoveService do
issue.move_to_end && issue.save!
end
params.merge!(move_after_iid: issue1.iid, move_before_iid: issue2.iid)
params.merge!(move_after_id: issue1.id, move_before_id: issue2.id)
described_class.new(project, user, params).execute(issue)
......
......@@ -80,7 +80,7 @@ describe Issues::UpdateService, :mailer do
issue.save
end
opts[:move_between_iids] = [issue1.iid, issue2.iid]
opts[:move_between_ids] = [issue1.id, issue2.id]
update_issue(opts)
......
......@@ -7,7 +7,7 @@
"skype":"aertert",
"linkedin":"",
"twitter":"",
"color_scheme_id":2,
"theme_id":2,"color_scheme_id":2,
"state":"active",
"created_at":"2012-12-21T13:02:20Z",
"extern_uid":null,
......
......@@ -7,7 +7,7 @@
"skype":"aertert",
"linkedin":"",
"twitter":"",
"color_scheme_id":2,
"theme_id":2,"color_scheme_id":2,
"state":"active",
"created_at":"2012-12-21T13:02:20Z",
"extern_uid":null,
......
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