Merge remote-tracking branch 'origin/master' into zj/gitlab-ce-zj-auto-devops-table

This commit is contained in:
Kamil Trzcinski 2017-09-07 18:03:20 +02:00
commit 12ddc28f84
169 changed files with 2090 additions and 1043 deletions

View File

@ -40,6 +40,7 @@ stages:
- test - test
- post-test - post-test
- pages - pages
- post-cleanup
# Predefined scopes # Predefined scopes
.dedicated-runner: &dedicated-runner .dedicated-runner: &dedicated-runner
@ -153,8 +154,7 @@ stages:
- master@gitlab/gitlabhq - master@gitlab/gitlabhq
- master@gitlab/gitlab-ee - master@gitlab/gitlab-ee
# Trigger a package build on omnibus-gitlab repository # Trigger a package build in omnibus-gitlab repository
build-package: build-package:
image: ruby:2.3-alpine image: ruby:2.3-alpine
before_script: [] before_script: []
@ -166,11 +166,47 @@ build-package:
cache: {} cache: {}
when: manual when: manual
script: script:
- scripts/trigger-build - scripts/trigger-build-omnibus
only: only:
- //@gitlab-org/gitlab-ce - //@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee - //@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 knapsack and rspec_flaky reports
retrieve-tests-metadata: retrieve-tests-metadata:
<<: *tests-metadata-state <<: *tests-metadata-state

View File

@ -5,7 +5,7 @@ By submitting code as an individual you agree to the
By submitting code as an entity 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). [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) - [Workflow labels](#workflow-labels)
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc) - [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) - [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) - [Priority labels (~Deliverable and ~Stretch)](#priority-labels-deliverable-and-stretch)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests) - [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design--ui-elements) - [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. - Type: ~"feature proposal", ~bug, ~customer, etc.
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, 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 - Priority: ~Deliverable, ~Stretch
All labels, their meaning and priority are defined on the 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. 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. Team labels specify what team is responsible for this issue.
Assigning a team label makes sure issues get the attention of the appropriate Assigning a team label makes sure issues get the attention of the appropriate
people. 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". ~Geo, ~Gitaly, ~Platform, ~Prometheus, ~Release, and ~"UX".
The descriptions on the [labels page][labels-page] explain what falls under the 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 [weight](#issue-weight) of the issue. We use issue weight to let contributors
know how difficult the issue is. Additionally: 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 as suitable for people that have never contributed to GitLab before on the
[Up For Grabs campaign](http://up-for-grabs.net) [Up For Grabs campaign](http://up-for-grabs.net)
- We encourage people that have never contributed to any open source project to - 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 [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 [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

View File

@ -6,7 +6,8 @@ const Api = {
namespacesPath: '/api/:version/namespaces.json', namespacesPath: '/api/:version/namespaces.json',
groupProjectsPath: '/api/:version/groups/:id/projects.json', groupProjectsPath: '/api/:version/groups/:id/projects.json',
projectsPath: '/api/:version/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', licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key', gitignorePath: '/api/:version/templates/gitignores/:key',
gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key', gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key',
@ -74,9 +75,16 @@ const Api = {
}, },
newLabel(namespacePath, projectPath, data, callback) { newLabel(namespacePath, projectPath, data, callback) {
const url = Api.buildUrl(Api.labelsPath) let url;
.replace(':namespace_path', namespacePath)
.replace(':project_path', projectPath); 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({ return $.ajax({
url, url,
type: 'POST', type: 'POST',

View File

@ -53,7 +53,8 @@ $(() => {
data: { data: {
state: Store.state, state: Store.state,
loading: true, loading: true,
endpoint: $boardApp.dataset.endpoint, boardsEndpoint: $boardApp.dataset.boardsEndpoint,
listsEndpoint: $boardApp.dataset.listsEndpoint,
boardId: $boardApp.dataset.boardId, boardId: $boardApp.dataset.boardId,
disabled: $boardApp.dataset.disabled === 'true', disabled: $boardApp.dataset.disabled === 'true',
issueLinkBase: $boardApp.dataset.issueLinkBase, issueLinkBase: $boardApp.dataset.issueLinkBase,
@ -68,7 +69,13 @@ $(() => {
}, },
}, },
created () { 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 = new FilteredSearchBoards(Store.filter, true);
this.filterManager.setup(); this.filterManager.setup();
@ -112,19 +119,21 @@ $(() => {
gl.IssueBoardsSearch = new Vue({ gl.IssueBoardsSearch = new Vue({
el: document.getElementById('js-add-list'), el: document.getElementById('js-add-list'),
data: { data: {
filters: Store.state.filters filters: Store.state.filters,
}, },
mounted () { mounted () {
gl.issueBoards.newListDropdownInit(); gl.issueBoards.newListDropdownInit();
} },
}); });
gl.IssueBoardsModalAddBtn = new Vue({ gl.IssueBoardsModalAddBtn = new Vue({
mixins: [gl.issueBoards.ModalMixins], mixins: [gl.issueBoards.ModalMixins],
el: document.getElementById('js-add-issues-btn'), el: document.getElementById('js-add-issues-btn'),
data: { data() {
modal: ModalStore.store, return {
store: Store.state, modal: ModalStore.store,
store: Store.state,
};
}, },
watch: { watch: {
disabled() { disabled() {
@ -133,6 +142,9 @@ $(() => {
}, },
computed: { computed: {
disabled() { disabled() {
if (!this.store) {
return true;
}
return !this.store.lists.filter(list => !list.preset).length; return !this.store.lists.filter(list => !list.preset).length;
}, },
tooltipTitle() { tooltipTitle() {
@ -145,7 +157,7 @@ $(() => {
}, },
methods: { methods: {
updateTooltip() { updateTooltip() {
const $tooltip = $(this.$el); const $tooltip = $(this.$refs.addIssuesButton);
this.$nextTick(() => { this.$nextTick(() => {
if (this.disabled) { if (this.disabled) {
@ -165,16 +177,19 @@ $(() => {
this.updateTooltip(); this.updateTooltip();
}, },
template: ` template: `
<button <div class="board-extra-actions">
class="btn btn-create pull-right prepend-left-10" <button
type="button" class="btn btn-create prepend-left-10"
data-placement="bottom" type="button"
:class="{ 'disabled': disabled }" data-placement="bottom"
:title="tooltipTitle" ref="addIssuesButton"
:aria-disabled="disabled" :class="{ 'disabled': disabled }"
@click="openModal"> :title="tooltipTitle"
Add issues :aria-disabled="disabled"
</button> @click="openModal">
Add issues
</button>
</div>
`, `,
}); });
}); });

View File

@ -77,7 +77,7 @@ export default {
this.showIssueForm = !this.showIssueForm; this.showIssueForm = !this.showIssueForm;
}, },
onScroll() { onScroll() {
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) { if (!this.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) {
this.loadNextPage(); this.loadNextPage();
} }
}, },
@ -165,11 +165,9 @@ export default {
v-if="loading"> v-if="loading">
<loading-icon /> <loading-icon />
</div> </div>
<transition name="slide-down"> <board-new-issue
<board-new-issue :list="list"
:list="list" v-if="list.type !== 'closed' && showIssueForm"/>
v-if="list.type !== 'closed' && showIssueForm"/>
</transition>
<ul <ul
class="board-list" class="board-list"
v-show="!loading" v-show="!loading"

View File

@ -6,7 +6,10 @@ const Store = gl.issueBoards.BoardsStore;
export default { export default {
name: 'BoardNewIssue', name: 'BoardNewIssue',
props: { props: {
list: Object, list: {
type: Object,
required: true,
},
}, },
data() { data() {
return { return {

View File

@ -64,10 +64,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({
return this.issue.assignees.length > this.numberOverLimit; return this.issue.assignees.length > this.numberOverLimit;
}, },
cardUrl() { cardUrl() {
return `${this.issueLinkBase}/${this.issue.id}`; return `${this.issueLinkBase}/${this.issue.iid}`;
}, },
issueId() { issueId() {
return `#${this.issue.id}`; if (this.issue.iid) {
return `#${this.issue.iid}`;
}
return false;
}, },
showLabelFooter() { showLabelFooter() {
return this.issue.labels.find(l => this.showLabel(l)) !== undefined; 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> :title="issue.title">{{ issue.title }}</a>
<span <span
class="card-number" class="card-number"
v-if="issue.id" v-if="issueId"
> >
{{ issueId }} {{ issueId }}
</span> </span>

View File

@ -29,7 +29,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
const firstListIndex = 1; const firstListIndex = 1;
const list = this.modal.selectedList || this.state.lists[firstListIndex]; const list = this.modal.selectedList || this.state.lists[firstListIndex];
const selectedIssues = ModalStore.getSelectedIssues(); const selectedIssues = ModalStore.getSelectedIssues();
const issueIds = selectedIssues.map(issue => issue.globalId); const issueIds = selectedIssues.map(issue => issue.id);
// Post the data to the backend // Post the data to the backend
gl.boardService.bulkUpdate(issueIds, { gl.boardService.bulkUpdate(issueIds, {

View File

@ -27,7 +27,7 @@ gl.issueBoards.newListDropdownInit = () => {
$this.glDropdown({ $this.glDropdown({
data(term, callback) { data(term, callback) {
$.get($this.attr('data-labels')) $.get($this.attr('data-list-labels-path'))
.then((resp) => { .then((resp) => {
callback(resp); callback(resp);
}); });

View File

@ -18,17 +18,33 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
type: Object, type: Object,
required: true, required: true,
}, },
issueUpdate: {
type: String,
required: true,
},
},
computed: {
updateUrl() {
return this.issueUpdate;
},
}, },
methods: { methods: {
removeIssue() { removeIssue() {
const issue = this.issue; const issue = this.issue;
const lists = issue.getLists(); const lists = issue.getLists();
const labelIds = lists.map(list => list.label.id); const listLabelIds = lists.map(list => list.label.id);
let labelIds = this.issue.labels
// Post the remove data .map(label => label.id)
gl.boardService.bulkUpdate([issue.globalId], { .filter(id => !listLabelIds.includes(id));
remove_label_ids: labelIds, if (labelIds.length === 0) {
}).catch(() => { 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'); new Flash('Failed to remove issue from board, please try again.', 'alert');
lists.forEach((list) => { lists.forEach((list) => {

View File

@ -7,8 +7,8 @@ import Vue from 'vue';
class ListIssue { class ListIssue {
constructor (obj, defaultAvatar) { constructor (obj, defaultAvatar) {
this.globalId = obj.id; this.id = obj.id;
this.id = obj.iid; this.iid = obj.iid;
this.title = obj.title; this.title = obj.title;
this.confidential = obj.confidential; this.confidential = obj.confidential;
this.dueDate = obj.due_date; this.dueDate = obj.due_date;

View File

@ -4,6 +4,7 @@ class ListLabel {
constructor (obj) { constructor (obj) {
this.id = obj.id; this.id = obj.id;
this.title = obj.title; this.title = obj.title;
this.type = obj.type;
this.color = obj.color; this.color = obj.color;
this.textColor = obj.text_color; this.textColor = obj.text_color;
this.description = obj.description; this.description = obj.description;

View File

@ -110,11 +110,13 @@ class List {
return gl.boardService.newIssue(this.id, issue) return gl.boardService.newIssue(this.id, issue)
.then(resp => resp.json()) .then(resp => resp.json())
.then((data) => { .then((data) => {
issue.id = data.iid; issue.id = data.id;
issue.iid = data.iid;
issue.project = data.project;
if (this.issuesSize > 1) { if (this.issuesSize > 1) {
const moveBeforeIid = this.issues[1].id; const moveBeforeId = this.issues[1].id;
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid); gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeId);
} }
}); });
} }
@ -126,19 +128,19 @@ class List {
} }
addIssue (issue, listFrom, newIndex) { addIssue (issue, listFrom, newIndex) {
let moveBeforeIid = null; let moveBeforeId = null;
let moveAfterIid = null; let moveAfterId = null;
if (!this.findIssue(issue.id)) { if (!this.findIssue(issue.id)) {
if (newIndex !== undefined) { if (newIndex !== undefined) {
this.issues.splice(newIndex, 0, issue); this.issues.splice(newIndex, 0, issue);
if (this.issues[newIndex - 1]) { if (this.issues[newIndex - 1]) {
moveBeforeIid = this.issues[newIndex - 1].id; moveBeforeId = this.issues[newIndex - 1].id;
} }
if (this.issues[newIndex + 1]) { if (this.issues[newIndex + 1]) {
moveAfterIid = this.issues[newIndex + 1].id; moveAfterId = this.issues[newIndex + 1].id;
} }
} else { } else {
this.issues.push(issue); this.issues.push(issue);
@ -151,30 +153,30 @@ class List {
if (listFrom) { if (listFrom) {
this.issuesSize += 1; 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(oldIndex, 1);
this.issues.splice(newIndex, 0, issue); 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(() => { .catch(() => {
// TODO: handle request error // TODO: handle request error
}); });
} }
updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) { updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) {
gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid) gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId)
.catch(() => { .catch(() => {
// TODO: handle request error // TODO: handle request error
}); });
} }
findIssue (id) { findIssue (id) {
return this.issues.filter(issue => issue.id === id)[0]; return this.issues.find(issue => issue.id === id);
} }
removeIssue (removeIssue) { removeIssue (removeIssue) {

View File

@ -3,21 +3,21 @@
import Vue from 'vue'; import Vue from 'vue';
class BoardService { class BoardService {
constructor (root, bulkUpdatePath, boardId) { constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
this.boards = Vue.resource(`${root}{/id}.json`, {}, { this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
issues: { issues: {
method: 'GET', 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: { generate: {
method: 'POST', method: 'POST',
url: `${root}/${boardId}/lists/generate.json` url: `${listsEndpoint}/generate.json`
} }
}); });
this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {}); this.issue = Vue.resource(`${gon.relative_url_root}/boards/${boardId}/issues{/id}`, {});
this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}, { this.issues = Vue.resource(`${listsEndpoint}{/id}/issues`, {}, {
bulkUpdate: { bulkUpdate: {
method: 'POST', method: 'POST',
url: bulkUpdatePath, url: bulkUpdatePath,
@ -60,12 +60,12 @@ class BoardService {
return this.issues.get(data); 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 }, { return this.issue.update({ id }, {
from_list_id, from_list_id,
to_list_id, to_list_id,
move_before_iid, move_before_id,
move_after_iid, move_after_id,
}); });
} }

View File

@ -21,8 +21,10 @@ let headerHeight = 50;
export const getHeaderHeight = () => headerHeight; export const getHeaderHeight = () => headerHeight;
export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-icons-only');
export const canShowActiveSubItems = (el) => { export const canShowActiveSubItems = (el) => {
if (el.classList.contains('active') && (sidebar && !sidebar.classList.contains('sidebar-icons-only'))) { if (el.classList.contains('active') && !isSidebarCollapsed()) {
return false; return false;
} }
@ -100,12 +102,13 @@ export const moveSubItemsToPosition = (el, subItems) => {
export const showSubLevelItems = (el) => { export const showSubLevelItems = (el) => {
const subItems = el.querySelector('.sidebar-sub-level-items'); const subItems = el.querySelector('.sidebar-sub-level-items');
const isIconOnly = subItems && subItems.classList.contains('is-fly-out-only');
if (!canShowSubItems() || !canShowActiveSubItems(el)) return; if (!canShowSubItems() || !canShowActiveSubItems(el)) return;
el.classList.add(IS_OVER_CLASS); el.classList.add(IS_OVER_CLASS);
if (!subItems) return; if (!subItems || (!isSidebarCollapsed() && isIconOnly)) return;
subItems.style.display = 'block'; subItems.style.display = 'block';
el.classList.add(IS_SHOWING_FLY_OUT_CLASS); el.classList.add(IS_SHOWING_FLY_OUT_CLASS);

View File

@ -73,7 +73,7 @@ class Issue {
$(document).trigger('issuable:change', isClosed); $(document).trigger('issuable:change', isClosed);
this.toggleCloseReopenButton(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; numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues)); projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues));

View File

@ -19,6 +19,7 @@
@import "framework/flash"; @import "framework/flash";
@import "framework/forms"; @import "framework/forms";
@import "framework/gfm"; @import "framework/gfm";
@import "framework/gitlab-theme";
@import "framework/header"; @import "framework/header";
@import "framework/highlight"; @import "framework/highlight";
@import "framework/issue_box"; @import "framework/issue_box";

View File

@ -412,11 +412,12 @@ table {
.gl-accessibility { .gl-accessibility {
&:focus { &:focus {
display: flex;
align-items: center;
top: 1px; top: 1px;
left: 1px; left: 1px;
width: auto; width: auto;
height: 100%; height: 100%;
line-height: 50px;
padding: 0 10px; padding: 0 10px;
clip: auto; clip: auto;
text-decoration: none; text-decoration: none;

View File

@ -183,7 +183,7 @@
width: auto; width: auto;
top: 100%; top: 100%;
left: 0; left: 0;
z-index: 200; z-index: 300;
min-width: 240px; min-width: 240px;
max-width: 500px; max-width: 500px;
margin-top: 2px; 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('.breadcrumbs-list .dropdown ');
@include new-style-dropdown('.js-namespace-select + '); @include new-style-dropdown('.js-namespace-select + ');
header.navbar-gitlab-new .header-content .dropdown-menu.projects-dropdown-menu { header.navbar-gitlab-new .header-content .dropdown-menu.projects-dropdown-menu {
padding: 0; padding: 0;
@media (max-width: $screen-xs-max) {
display: table;
left: -50px;
min-width: 300px;
}
} }
.projects-dropdown-container { .projects-dropdown-container {

View File

@ -0,0 +1,265 @@
/**
* 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;
}
}
}
}

View File

@ -111,7 +111,6 @@ header {
svg { svg {
height: 16px; height: 16px;
width: 23px; width: 23px;
fill: currentColor;
} }
} }

View File

@ -74,6 +74,8 @@ $red-700: #a62d19;
$red-800: #8b2615; $red-800: #8b2615;
$red-900: #711e11; $red-900: #711e11;
// GitLab themes
$indigo-50: #f7f7ff; $indigo-50: #f7f7ff;
$indigo-100: #ebebfa; $indigo-100: #ebebfa;
$indigo-200: #d1d1f0; $indigo-200: #d1d1f0;
@ -86,6 +88,43 @@ $indigo-800: #393982;
$indigo-900: #292961; $indigo-900: #292961;
$indigo-950: #1a1a40; $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: #000;
$black-transparent: rgba(0, 0, 0, 0.3); $black-transparent: rgba(0, 0, 0, 0.3);
$almost-black: #242424; $almost-black: #242424;

View File

@ -9,10 +9,20 @@
header.navbar-gitlab-new { header.navbar-gitlab-new {
color: $white-light; color: $white-light;
background: linear-gradient(to right, $indigo-900, $indigo-800);
border-bottom: 0; border-bottom: 0;
min-height: $new-navbar-height; min-height: $new-navbar-height;
.logo-text {
line-height: initial;
svg {
width: 55px;
height: 14px;
margin: 0;
fill: $white-light;
}
}
.header-content { .header-content {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
@ -38,10 +48,10 @@ header.navbar-gitlab-new {
img { img {
height: 28px; height: 28px;
margin-right: 10px; margin-right: 8px;
} }
> a { a {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
align-items: center; align-items: center;
@ -54,22 +64,6 @@ header.navbar-gitlab-new {
margin-right: 8px; 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 { .navbar-collapse {
padding-left: 0; padding-left: 0;
color: $indigo-200;
box-shadow: 0; box-shadow: 0;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
@ -132,7 +125,6 @@ header.navbar-gitlab-new {
font-size: 14px; font-size: 14px;
text-align: center; text-align: center;
color: currentColor; color: currentColor;
border-left: 1px solid lighten($indigo-700, 10%);
&:hover, &:hover,
&:focus, &:focus,
@ -167,63 +159,49 @@ header.navbar-gitlab-new {
will-change: color; will-change: color;
margin: 4px 2px; margin: 4px 2px;
padding: 6px 8px; padding: 6px 8px;
color: $indigo-200;
height: 32px; height: 32px;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
padding: 0; padding: 0;
} }
svg {
fill: $indigo-200;
}
&.header-user-dropdown-toggle { &.header-user-dropdown-toggle {
margin-left: 2px; margin-left: 2px;
.header-user-avatar { .header-user-avatar {
border-color: $indigo-200;
margin-right: 0; margin-right: 0;
} }
} }
&:hover,
&:focus {
text-decoration: none;
outline: 0;
opacity: 1;
color: $white-light;
svg {
fill: currentColor;
}
&.header-user-dropdown-toggle {
.header-user-avatar {
border-color: $white-light;
}
}
}
} }
.header-new-dropdown-toggle { .header-new-dropdown-toggle {
margin-right: 0; margin-right: 0;
} }
> a:hover,
> a: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;
}
&.header-user-dropdown-toggle {
.header-user-avatar {
border-color: $white-light;
}
}
}
.impersonated-user, .impersonated-user,
.impersonated-user:hover { .impersonated-user:hover {
margin-right: 1px; margin-right: 1px;
background-color: $white-light; background-color: $white-light;
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
svg {
fill: $indigo-900;
}
} }
.impersonation-btn, .impersonation-btn,
@ -241,8 +219,6 @@ header.navbar-gitlab-new {
&.active > a, &.active > a,
&.dropdown.open > a { &.dropdown.open > a {
color: $indigo-900;
background-color: $white-light;
svg { svg {
fill: currentColor; fill: currentColor;
@ -256,7 +232,6 @@ header.navbar-gitlab-new {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
margin: 0 0 0 6px; margin: 0 0 0 6px;
color: $indigo-200;
.dropdown-chevron { .dropdown-chevron {
position: relative; position: relative;
@ -274,17 +249,6 @@ header.navbar-gitlab-new {
text-decoration: none; text-decoration: none;
outline: 0; outline: 0;
color: $white-light; 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 { svg {
fill: currentColor; fill: currentColor;
@ -309,7 +273,6 @@ header.navbar-gitlab-new {
} }
&.line-separator { &.line-separator {
border-left: 1px solid rgba($indigo-200, .2);
margin: 8px; margin: 8px;
} }
} }
@ -339,17 +302,14 @@ header.navbar-gitlab-new {
height: 32px; height: 32px;
border: 0; border: 0;
border-radius: $border-radius-default; 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; transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s;
&:hover { &:hover {
background-color: rgba($indigo-200, .3);
box-shadow: none; box-shadow: none;
} }
} }
&.search-active form { &.search-active form {
background-color: $white-light;
box-shadow: none; box-shadow: none;
.search-input { .search-input {
@ -377,43 +337,26 @@ header.navbar-gitlab-new {
} }
.search-input::placeholder { .search-input::placeholder {
color: rgba($indigo-200, .8);
transition: color ease-in-out 0.15s; transition: color ease-in-out 0.15s;
} }
.location-badge { .location-badge {
font-size: 12px; font-size: 12px;
color: $indigo-100;
background-color: rgba($indigo-200, .1);
will-change: color;
margin: -4px 4px -4px -4px; margin: -4px 4px -4px -4px;
line-height: 25px; line-height: 25px;
padding: 4px 8px; padding: 4px 8px;
border-radius: 2px 0 0 2px; border-radius: 2px 0 0 2px;
border-right: 1px solid $indigo-800;
height: 32px; height: 32px;
transition: border-color ease-in-out 0.15s; transition: border-color ease-in-out 0.15s;
} }
.search-input-wrap {
.search-icon,
.clear-icon {
color: rgba($indigo-200, .8);
}
}
&.search-active { &.search-active {
.location-badge { .location-badge {
color: $gl-text-color;
background-color: $nav-badge-bg; background-color: $nav-badge-bg;
border-color: $border-color; border-color: $border-color;
} }
.search-input-wrap { .search-input-wrap {
.search-icon {
color: rgba($indigo-200, .8);
}
.clear-icon { .clear-icon {
color: $white-light; color: $white-light;
} }
@ -517,8 +460,6 @@ header.navbar-gitlab-new {
.btn-sign-in { .btn-sign-in {
margin-top: 3px; margin-top: 3px;
background-color: $indigo-100;
color: $indigo-900;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
&:hover { &:hover {

View File

@ -106,11 +106,8 @@ $new-sidebar-collapsed-width: 50px;
overflow-x: hidden; overflow-x: hidden;
} }
.badge, .badge:not(.fly-out-badge),
.sidebar-context-title { .sidebar-context-title,
display: none;
}
.nav-item-name { .nav-item-name {
display: none; display: none;
} }
@ -118,6 +115,10 @@ $new-sidebar-collapsed-width: 50px;
.sidebar-top-level-items > li > a { .sidebar-top-level-items > li > a {
min-height: 44px; min-height: 44px;
} }
.fly-out-top-item {
display: block;
}
} }
&.nav-sidebar-expanded { &.nav-sidebar-expanded {
@ -154,16 +155,9 @@ $new-sidebar-collapsed-width: 50px;
} }
li.active { li.active {
box-shadow: inset 4px 0 0 $active-border;
> a { > a {
color: $active-color;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
svg {
fill: $active-color;
}
} }
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
@ -179,6 +173,10 @@ $new-sidebar-collapsed-width: 50px;
width: 16px; width: 16px;
} }
} }
.fly-out-top-item {
display: none;
}
} }
.nav-sidebar-inner-scroll { .nav-sidebar-inner-scroll {
@ -249,7 +247,7 @@ $new-sidebar-collapsed-width: 50px;
left: $new-sidebar-width; left: $new-sidebar-width;
min-width: 150px; min-width: 150px;
margin-top: -1px; margin-top: -1px;
padding: 8px 1px; padding: 4px 1px;
background-color: $white-light; background-color: $white-light;
box-shadow: 2px 1px 3px $dropdown-shadow-color; box-shadow: 2px 1px 3px $dropdown-shadow-color;
border: 1px solid $gray-darker; border: 1px solid $gray-darker;
@ -270,6 +268,13 @@ $new-sidebar-collapsed-width: 50px;
margin-top: 1px; margin-top: 1px;
} }
.divider {
height: 1px;
margin: 4px -1px;
padding: 0;
background-color: $dropdown-divider-color;
}
> .active { > .active {
box-shadow: none; box-shadow: none;
@ -309,7 +314,7 @@ $new-sidebar-collapsed-width: 50px;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
.sidebar-sub-level-items { .sidebar-sub-level-items:not(.is-fly-out-only) {
display: block; 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 // Mobile nav

View File

@ -117,13 +117,12 @@
} }
.board-title { .board-title {
position: initial;
padding: 0; padding: 0;
border-bottom: 0; border-bottom: 0;
> span { > span {
display: block; display: block;
transform: rotate(90deg) translate(25px, 0); transform: rotate(90deg) translate(35px, 10px);
} }
} }
@ -151,11 +150,18 @@
} }
.board-header { .board-header {
border-top-left-radius: $border-radius-default; position: relative;
border-top-right-radius: $border-radius-default;
&.has-border { &.has-border::before {
border-top: 3px solid; 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-top: -1px;
margin-right: -1px; margin-right: -1px;
margin-left: -1px; margin-left: -1px;
@ -176,12 +182,16 @@
} }
.board-title { .board-title {
position: relative;
margin: 0; margin: 0;
padding: $gl-padding; padding: 12px $gl-padding;
padding-bottom: ($gl-padding + 3px);
font-size: 1em; font-size: 1em;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
display: flex;
align-items: center;
}
.board-title-text {
margin-right: auto;
} }
.board-delete { .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 { .board-list-component {
height: calc(100% - 49px); height: calc(100% - 49px);
overflow: hidden; overflow: hidden;
position: relative;
} }
.board-list { .board-list {
@ -429,7 +406,7 @@
} }
.board-new-issue-form { .board-new-issue-form {
z-index: 1; z-index: 4;
margin: 5px; margin: 5px;
} }

View File

@ -1,3 +1,67 @@
@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 { .syntax-theme {
label { label {
margin-right: 20px; margin-right: 20px;

View File

@ -166,7 +166,7 @@ input[type="checkbox"]:hover {
.dropdown-menu { .dropdown-menu {
transition-duration: 100ms, 75ms; transition-duration: 100ms, 75ms;
transition-delay: 75ms, 100ms; transition-delay: 75ms, 100ms;
transform: translateY(13px); transform: translateY(7px);
opacity: 1; opacity: 1;
} }
} }

View File

@ -211,6 +211,7 @@ class Admin::UsersController < Admin::ApplicationController
:provider, :provider,
:remember_me, :remember_me,
:skype, :skype,
:theme_id,
:twitter, :twitter,
:username, :username,
:website_url :website_url

View File

@ -0,0 +1,21 @@
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

View File

@ -0,0 +1,90 @@
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

View File

@ -0,0 +1,75 @@
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

View File

@ -0,0 +1,42 @@
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

View File

@ -35,7 +35,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:color_scheme_id, :color_scheme_id,
:layout, :layout,
:dashboard, :dashboard,
:project_view :project_view,
:theme_id
) )
end end
end end

View File

@ -1,15 +0,0 @@
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

View File

@ -1,94 +0,0 @@
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

View File

@ -1,86 +0,0 @@
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

View File

@ -1,32 +1,31 @@
class Projects::BoardsController < Projects::ApplicationController class Projects::BoardsController < Projects::ApplicationController
include BoardsResponses
include IssuableCollections include IssuableCollections
before_action :authorize_read_board!, only: [:index, :show] before_action :authorize_read_board!, only: [:index, :show]
before_action :assign_endpoint_vars
def index def index
@boards = ::Boards::ListService.new(project, current_user).execute @boards = Boards::ListService.new(project, current_user).execute
respond_to do |format| respond_with_boards
format.html
format.json do
render json: serialize_as_json(@boards)
end
end
end end
def show def show
@board = project.boards.find(params[:id]) @board = project.boards.find(params[:id])
respond_to do |format| respond_with_board
format.html
format.json do
render json: serialize_as_json(@board)
end
end
end end
private 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! def authorize_read_board!
return access_denied! unless can?(current_user, :read_board, project) return access_denied! unless can?(current_user, :read_board, project)
end end

View File

@ -1,15 +1,80 @@
module BoardsHelper module BoardsHelper
def board_data def board
board = @board || @boards.first @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, board_id: board.id,
disabled: "#{!can?(current_user, :admin_list, @project)}", disabled: "#{!can?(current_user, :admin_list, current_board_parent)}",
issue_link_base: project_issues_path(@project), issue_link_base: build_issue_link_base,
root_path: root_path, 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) default_avatar: image_path(default_avatar)
} }
end 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 end

View File

@ -347,6 +347,14 @@ module IssuablesHelper
end end
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) def issuable_sidebar_options(issuable, can_edit_issuable)
{ {
endpoint: "#{issuable_json_path(issuable)}?basic=true", endpoint: "#{issuable_json_path(issuable)}?basic=true",

View File

@ -121,13 +121,14 @@ module LabelsHelper
end end
end end
def labels_filter_path def labels_filter_path(only_group_labels = false)
return group_labels_path(@group, :json) if @group
project = @target_project || @project project = @target_project || @project
if project if project
project_labels_path(project, :json) 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 else
dashboard_labels_path(:json) dashboard_labels_path(:json)
end end

View File

@ -40,6 +40,10 @@ module PreferencesHelper
] ]
end end
def user_application_theme
@user_application_theme ||= Gitlab::Themes.for_user(current_user).css_class
end
def user_color_scheme def user_color_scheme
Gitlab::ColorSchemes.for_user(current_user).css_class Gitlab::ColorSchemes.for_user(current_user).css_class
end end

View File

@ -134,19 +134,21 @@ module SearchHelper
end end
def search_filter_input_options(type) def search_filter_input_options(type)
opts = { opts =
id: "filtered-search-#{type}", {
placeholder: 'Search or filter results...', id: "filtered-search-#{type}",
data: { placeholder: 'Search or filter results...',
'username-params' => @users.to_json(only: [:id, :username]) data: {
'username-params' => @users.to_json(only: [:id, :username])
}
} }
}
if @project.present? if @project.present?
opts[:data]['project-id'] = @project.id opts[:data]['project-id'] = @project.id
opts[:data]['base-endpoint'] = project_path(@project) opts[:data]['base-endpoint'] = project_path(@project)
else else
# Group context # Group context
opts[:data]['group-id'] = @group.id
opts[:data]['base-endpoint'] = group_canonical_path(@group) opts[:data]['base-endpoint'] = group_canonical_path(@group)
end end

View File

@ -3,7 +3,19 @@ class Board < ActiveRecord::Base
has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent 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 def backlog_list
lists.merge(List.backlog).take lists.merge(List.backlog).take

View File

@ -10,8 +10,12 @@ module RelativePositioning
after_save :save_positionable_neighbours after_save :save_positionable_neighbours
end end
def project_ids
[project.id]
end
def max_relative_position def max_relative_position
self.class.in_projects(project.id).maximum(:relative_position) self.class.in_projects(project_ids).maximum(:relative_position)
end end
def prev_relative_position def prev_relative_position
@ -19,7 +23,7 @@ module RelativePositioning
if self.relative_position if self.relative_position
prev_pos = self.class prev_pos = self.class
.in_projects(project.id) .in_projects(project_ids)
.where('relative_position < ?', self.relative_position) .where('relative_position < ?', self.relative_position)
.maximum(:relative_position) .maximum(:relative_position)
end end
@ -32,7 +36,7 @@ module RelativePositioning
if self.relative_position if self.relative_position
next_pos = self.class next_pos = self.class
.in_projects(project.id) .in_projects(project_ids)
.where('relative_position > ?', self.relative_position) .where('relative_position > ?', self.relative_position)
.minimum(:relative_position) .minimum(:relative_position)
end end
@ -59,7 +63,7 @@ module RelativePositioning
pos_after = before.next_relative_position pos_after = before.next_relative_position
if before.shift_after? 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 issue_to_move.move_after
@positionable_neighbours = [issue_to_move] @positionable_neighbours = [issue_to_move]
@ -74,7 +78,7 @@ module RelativePositioning
pos_before = after.prev_relative_position pos_before = after.prev_relative_position
if after.shift_before? 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 issue_to_move.move_before
@positionable_neighbours = [issue_to_move] @positionable_neighbours = [issue_to_move]

View File

@ -1,5 +1,6 @@
class Event < ActiveRecord::Base class Event < ActiveRecord::Base
include Sortable include Sortable
include IgnorableColumn
default_scope { reorder(nil).where.not(author_id: nil) } default_scope { reorder(nil).where.not(author_id: nil) }
CREATED = 1 CREATED = 1
@ -50,13 +51,9 @@ class Event < ActiveRecord::Base
belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
has_one :push_event_payload, foreign_key: :event_id has_one :push_event_payload, foreign_key: :event_id
# For Hash only
serialize :data # rubocop:disable Cop/ActiveRecordSerialize
# Callbacks # Callbacks
after_create :reset_project_activity after_create :reset_project_activity
after_create :set_last_repository_updated_at, if: :push? after_create :set_last_repository_updated_at, if: :push?
after_create :replicate_event_for_push_events_migration
# Scopes # Scopes
scope :recent, -> { reorder(id: :desc) } scope :recent, -> { reorder(id: :desc) }
@ -82,6 +79,10 @@ class Event < ActiveRecord::Base
self.inheritance_column = 'action' 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 class << self
def model_name def model_name
ActiveModel::Name.new(self, nil, 'event') ActiveModel::Name.new(self, nil, 'event')
@ -159,7 +160,7 @@ class Event < ActiveRecord::Base
end end
def push? def push?
action == PUSHED && valid_push? false
end end
def merged? def merged?
@ -272,87 +273,6 @@ class Event < ActiveRecord::Base
end end
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 def target_iid
target.respond_to?(:iid) ? target.iid : target_id target.respond_to?(:iid) ? target.iid : target_id
end end
@ -432,16 +352,6 @@ class Event < ActiveRecord::Base
user ? author_id == user.id : false user ? author_id == user.id : false
end 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 def to_partial_path
# We are intentionally using `Event` rather than `self.class` so that # We are intentionally using `Event` rather than `self.class` so that
# subclasses also use the `Event` implementation. # subclasses also use the `Event` implementation.

View File

@ -1,5 +0,0 @@
# 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

View File

@ -34,7 +34,8 @@ class Label < ActiveRecord::Base
scope :templates, -> { where(template: true) } scope :templates, -> { where(template: true) }
scope :with_title, ->(title) { where(title: title) } 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) def self.prioritized(project)
joins(:priorities) joins(:priorities)
@ -172,6 +173,7 @@ class Label < ActiveRecord::Base
def as_json(options = {}) def as_json(options = {})
super(options).tap do |json| super(options).tap do |json|
json[:type] = self.try(:type)
json[:priority] = priority(options[:project]) if options.key?(:project) json[:priority] = priority(options[:project]) if options.key?(:project)
end end
end end

View File

@ -1507,6 +1507,14 @@ class Project < ActiveRecord::Base
end end
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 def full_path_was
File.join(namespace.full_path, previous_changes['path'].first) File.join(namespace.full_path, previous_changes['path'].first)
end end

View File

@ -15,15 +15,21 @@ class PushEvent < Event
# should ensure the ID points to a valid project. # should ensure the ID points to a valid project.
validates :project_id, presence: true 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 # These fields are also not used for push events, thus storing them would be a
# waste. # waste.
validates :target_id, absence: true validates :target_id, absence: true
validates :target_type, 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 def self.sti_name
PUSHED PUSHED
end end
@ -36,86 +42,35 @@ class PushEvent < Event
!!(commit_from && commit_to) !!(commit_from && commit_to)
end 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? def valid_push?
return super unless push_event_payload
push_event_payload.ref.present? push_event_payload.ref.present?
end end
def new_ref? def new_ref?
return super unless push_event_payload
push_event_payload.created? push_event_payload.created?
end end
def rm_ref? def rm_ref?
return super unless push_event_payload
push_event_payload.removed? push_event_payload.removed?
end end
def commit_from def md_ref?
return super unless push_event_payload !(rm_ref? || new_ref?)
push_event_payload.commit_from
end
def commit_to
return super unless push_event_payload
push_event_payload.commit_to
end end
def ref_name def ref_name
return super unless push_event_payload
push_event_payload.ref push_event_payload.ref
end end
def ref_type alias_method :branch_name, :ref_name
return super unless push_event_payload alias_method :tag_name, :ref_name
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
def commit_id def commit_id
commit_to || commit_from commit_to || commit_from
end end
def commits_count def last_push_to_non_root?
return super unless push_event_payload branch? && project.default_branch != branch_name
push_event_payload.commit_count
end end
def validate_push_action def validate_push_action

View File

@ -35,6 +35,7 @@ class User < ActiveRecord::Base
default_value_for :project_view, :files default_value_for :project_view, :files
default_value_for :notified_of_own_activity, false default_value_for :notified_of_own_activity, false
default_value_for :preferred_language, I18n.default_locale default_value_for :preferred_language, I18n.default_locale
default_value_for :theme_id, gitlab_config.default_theme
attr_encrypted :otp_secret, attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base, key: Gitlab::Application.secrets.otp_key_base,

View File

@ -0,0 +1,10 @@
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

View File

@ -1,5 +1,5 @@
module Boards module Boards
class CreateService < BaseService class CreateService < Boards::BaseService
def execute def execute
create_board! if can_create_board? create_board! if can_create_board?
end end
@ -7,11 +7,11 @@ module Boards
private private
def can_create_board? def can_create_board?
project.boards.size == 0 parent.boards.size == 0
end end
def create_board! def create_board!
board = project.boards.create(params) board = parent.boards.create(params)
if board.persisted? if board.persisted?
board.lists.create(list_type: :backlog) board.lists.create(list_type: :backlog)

View File

@ -1,6 +1,14 @@
module Boards module Boards
module Issues 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 def execute
create_issue(params.merge(label_ids: [list.label_id])) create_issue(params.merge(label_ids: [list.label_id]))
end end
@ -8,7 +16,7 @@ module Boards
private private
def board def board
@board ||= project.boards.find(params.delete(:board_id)) @board ||= parent.boards.find(params.delete(:board_id))
end end
def list def list

View File

@ -1,6 +1,6 @@
module Boards module Boards
module Issues module Issues
class ListService < BaseService class ListService < Boards::BaseService
def execute def execute
issues = IssuesFinder.new(current_user, filter_params).execute issues = IssuesFinder.new(current_user, filter_params).execute
issues = without_board_labels(issues) unless movable_list? || closed_list? issues = without_board_labels(issues) unless movable_list? || closed_list?
@ -11,7 +11,7 @@ module Boards
private private
def board def board
@board ||= project.boards.find(params[:board_id]) @board ||= parent.boards.find(params[:board_id])
end end
def list def list
@ -33,14 +33,14 @@ module Boards
end end
def filter_params def filter_params
set_project set_parent
set_state set_state
params params
end end
def set_project def set_parent
params[:project_id] = project.id params[:project_id] = parent.id
end end
def set_state def set_state

View File

@ -1,17 +1,17 @@
module Boards module Boards
module Issues module Issues
class MoveService < BaseService class MoveService < Boards::BaseService
def execute(issue) def execute(issue)
return false unless can?(current_user, :update_issue, issue) return false unless can?(current_user, :update_issue, issue)
return false if issue_params.empty? return false if issue_params.empty?
update_service.execute(issue) update(issue)
end end
private private
def board def board
@board ||= project.boards.find(params[:board_id]) @board ||= parent.boards.find(params[:board_id])
end end
def move_between_lists? def move_between_lists?
@ -27,8 +27,8 @@ module Boards
@moving_to_list ||= board.lists.find_by(id: params[:to_list_id]) @moving_to_list ||= board.lists.find_by(id: params[:to_list_id])
end end
def update_service def update(issue)
::Issues::UpdateService.new(project, current_user, issue_params) ::Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue)
end end
def issue_params def issue_params
@ -42,7 +42,7 @@ module Boards
) )
end end
attrs[:move_between_iids] = move_between_iids if move_between_iids attrs[:move_between_ids] = move_between_ids if move_between_ids
attrs attrs
end end
@ -61,16 +61,16 @@ module Boards
if moving_to_list.movable? if moving_to_list.movable?
moving_from_list.label_id moving_from_list.label_id
else else
Label.on_project_boards(project.id).pluck(:label_id) Label.on_project_boards(parent.id).pluck(:label_id)
end end
Array(label_ids).compact Array(label_ids).compact
end end
def move_between_iids def move_between_ids
return unless params[:move_after_iid] || params[:move_before_iid] 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 end
end end

View File

@ -1,14 +1,14 @@
module Boards module Boards
class ListService < BaseService class ListService < Boards::BaseService
def execute def execute
create_board! if project.boards.empty? create_board! if parent.boards.empty?
project.boards parent.boards
end end
private private
def create_board! def create_board!
Boards::CreateService.new(project, current_user).execute Boards::CreateService.new(parent, current_user).execute
end end
end end
end end

View File

@ -1,19 +1,18 @@
module Boards module Boards
module Lists module Lists
class CreateService < BaseService class CreateService < Boards::BaseService
def execute(board) def execute(board)
List.transaction do List.transaction do
label = available_labels.find(params[:label_id]) label = available_labels_for(board).find(params[:label_id])
position = next_position(board) position = next_position(board)
create_list(board, label, position) create_list(board, label, position)
end end
end end
private private
def available_labels def available_labels_for(board)
LabelsFinder.new(current_user, project_id: project.id).execute LabelsFinder.new(current_user, project_id: parent.id).execute
end end
def next_position(board) def next_position(board)

View File

@ -1,6 +1,6 @@
module Boards module Boards
module Lists module Lists
class DestroyService < BaseService class DestroyService < Boards::BaseService
def execute(list) def execute(list)
return false unless list.destroyable? return false unless list.destroyable?

View File

@ -1,6 +1,6 @@
module Boards module Boards
module Lists module Lists
class GenerateService < BaseService class GenerateService < Boards::BaseService
def execute(board) def execute(board)
return false unless board.lists.movable.empty? return false unless board.lists.movable.empty?
@ -15,11 +15,11 @@ module Boards
def create_list(board, params) def create_list(board, params)
label = find_or_create_label(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 end
def find_or_create_label(params) def find_or_create_label(params)
::Labels::FindOrCreateService.new(current_user, project, params).execute ::Labels::FindOrCreateService.new(current_user, parent, params).execute
end end
def label_params def label_params

View File

@ -1,6 +1,6 @@
module Boards module Boards
module Lists module Lists
class ListService < BaseService class ListService < Boards::BaseService
def execute(board) def execute(board)
board.lists.create(list_type: :backlog) unless board.lists.backlog.exists? board.lists.create(list_type: :backlog) unless board.lists.backlog.exists?

View File

@ -1,6 +1,6 @@
module Boards module Boards
module Lists module Lists
class MoveService < BaseService class MoveService < Boards::BaseService
def execute(list) def execute(list)
@board = list.board @board = list.board
@old_position = list.position @old_position = list.position

View File

@ -3,7 +3,7 @@ module Issues
include SpamCheckService include SpamCheckService
def execute(issue) def execute(issue)
handle_move_between_iids(issue) handle_move_between_ids(issue)
filter_spam_check_params filter_spam_check_params
change_issue_duplicate(issue) change_issue_duplicate(issue)
move_issue_to_new_project(issue) || update(issue) move_issue_to_new_project(issue) || update(issue)
@ -54,13 +54,13 @@ module Issues
end end
end end
def handle_move_between_iids(issue) def handle_move_between_ids(issue)
return unless params[:move_between_iids] 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_before = get_issue_if_allowed(issue.project, before_id) if before_id
issue_after = get_issue_if_allowed(issue.project, after_iid) if after_iid issue_after = get_issue_if_allowed(issue.project, after_id) if after_id
issue.move_between(issue_before, issue_after) issue.move_between(issue_before, issue_after)
end end
@ -87,8 +87,8 @@ module Issues
private private
def get_issue_if_allowed(project, iid) def get_issue_if_allowed(project, id)
issue = project.issues.find_by(iid: iid) issue = project.issues.find(id)
issue if can?(current_user, :update_issue, issue) issue if can?(current_user, :update_issue, issue)
end end

View File

@ -1,7 +1,7 @@
!!! 5 !!! 5
%html{ lang: I18n.locale, class: page_class } %html{ lang: I18n.locale, class: page_class }
= render "layouts/head" = 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 "layouts/init_auto_complete" if @gfm_form
= render 'peek/bar' = render 'peek/bar'
= render "layouts/header/default" = render "layouts/header/default"

View File

@ -1,5 +1,5 @@
%ul.list-unstyled.navbar-sub-nav %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" } } %a{ href: "#", data: { toggle: "dropdown" } }
Projects Projects
= custom_icon('caret_down') = custom_icon('caret_down')
@ -22,7 +22,7 @@
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
Snippets Snippets
%li.dropdown.hidden-lg %li.header-more.dropdown.hidden-lg
%a{ href: "#", data: { toggle: "dropdown" } } %a{ href: "#", data: { toggle: "dropdown" } }
More More
= custom_icon('caret_down') = custom_icon('caret_down')

View File

@ -14,6 +14,11 @@
Overview Overview
%ul.sidebar-sub-level-items %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 = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview' do = link_to admin_root_path, title: 'Overview' do
%span %span
@ -55,6 +60,11 @@
Monitoring Monitoring
%ul.sidebar-sub-level-items %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 = nav_link(controller: :system_info) do
= link_to admin_system_info_path, title: 'System Info' do = link_to admin_system_info_path, title: 'System Info' do
%span %span
@ -82,6 +92,11 @@
= custom_icon('messages') = custom_icon('messages')
%span.nav-item-name %span.nav-item-name
Messages 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 = nav_link(controller: [:hooks, :hook_logs]) do
= sidebar_link admin_hooks_path, title: _('Hooks') do = sidebar_link admin_hooks_path, title: _('Hooks') do
@ -89,6 +104,11 @@
= custom_icon('system_hooks') = custom_icon('system_hooks')
%span.nav-item-name %span.nav-item-name
System Hooks 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 = nav_link(controller: :applications) do
= sidebar_link admin_applications_path, title: _('Applications') do = sidebar_link admin_applications_path, title: _('Applications') do
@ -96,6 +116,11 @@
= custom_icon('applications') = custom_icon('applications')
%span.nav-item-name %span.nav-item-name
Applications 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 = nav_link(controller: :abuse_reports) do
= sidebar_link admin_abuse_reports_path, title: _("Abuse Reports") do = sidebar_link admin_abuse_reports_path, title: _("Abuse Reports") do
@ -104,6 +129,12 @@
%span.nav-item-name %span.nav-item-name
Abuse Reports Abuse Reports
%span.badge.count= number_with_delimiter(AbuseReport.count(:all)) %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? - if akismet_enabled?
= nav_link(controller: :spam_logs) do = nav_link(controller: :spam_logs) do
@ -112,6 +143,11 @@
= custom_icon('spam_logs') = custom_icon('spam_logs')
%span.nav-item-name %span.nav-item-name
Spam Logs 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 = nav_link(controller: :deploy_keys) do
= sidebar_link admin_deploy_keys_path, title: _('Deploy Keys') do = sidebar_link admin_deploy_keys_path, title: _('Deploy Keys') do
@ -119,6 +155,11 @@
= custom_icon('key') = custom_icon('key')
%span.nav-item-name %span.nav-item-name
Deploy Keys 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 = nav_link(controller: :services) do
= sidebar_link admin_application_settings_services_path, title: _('Service Templates') do = sidebar_link admin_application_settings_services_path, title: _('Service Templates') do
@ -126,6 +167,11 @@
= custom_icon('service_templates') = custom_icon('service_templates')
%span.nav-item-name %span.nav-item-name
Service Templates 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 = nav_link(controller: :labels) do
= sidebar_link admin_labels_path, title: _('Labels') do = sidebar_link admin_labels_path, title: _('Labels') do
@ -133,6 +179,11 @@
= custom_icon('labels') = custom_icon('labels')
%span.nav-item-name %span.nav-item-name
Labels 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 = nav_link(controller: :appearances) do
= sidebar_link admin_appearances_path, title: _('Appearances') do = sidebar_link admin_appearances_path, title: _('Appearances') do
@ -140,6 +191,11 @@
= custom_icon('appearance') = custom_icon('appearance')
%span.nav-item-name %span.nav-item-name
Appearance 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 = nav_link(controller: :application_settings) do
= sidebar_link admin_application_settings_path, title: _('Settings') do = sidebar_link admin_application_settings_path, title: _('Settings') do
@ -147,5 +203,10 @@
= custom_icon('settings') = custom_icon('settings')
%span.nav-item-name %span.nav-item-name
Settings 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' = render 'shared/sidebar_toggle_button'

View File

@ -1,3 +1,6 @@
- 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{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll .nav-sidebar-inner-scroll
.context-header .context-header
@ -15,6 +18,11 @@
Overview Overview
%ul.sidebar-sub-level-items %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 = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do
= link_to group_path(@group), title: 'Group details' do = link_to group_path(@group), title: 'Group details' do
%span %span
@ -30,11 +38,16 @@
.nav-icon-container .nav-icon-container
= custom_icon('issues') = custom_icon('issues')
%span.nav-item-name %span.nav-item-name
- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
Issues Issues
%span.badge.count= number_with_delimiter(issues.count) %span.badge.count= number_with_delimiter(issues.count)
%ul.sidebar-sub-level-items %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 = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
= link_to issues_group_path(@group), title: 'List' do = link_to issues_group_path(@group), title: 'List' do
%span %span
@ -55,15 +68,25 @@
.nav-icon-container .nav-icon-container
= custom_icon('mr_bold') = custom_icon('mr_bold')
%span.nav-item-name %span.nav-item-name
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
Merge Requests Merge Requests
%span.badge.count= number_with_delimiter(merge_requests.count) %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 = nav_link(path: 'group_members#index') do
= sidebar_link group_group_members_path(@group), title: _('Members') do = sidebar_link group_group_members_path(@group), title: _('Members') do
.nav-icon-container .nav-icon-container
= custom_icon('members') = custom_icon('members')
%span.nav-item-name %span.nav-item-name
Members 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) - if current_user && can?(current_user, :admin_group, @group)
= nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do
= sidebar_link edit_group_path(@group), title: _('Settings') do = sidebar_link edit_group_path(@group), title: _('Settings') do
@ -72,6 +95,11 @@
%span.nav-item-name %span.nav-item-name
Settings Settings
%ul.sidebar-sub-level-items %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 = nav_link(path: 'groups#edit') do
= link_to edit_group_path(@group), title: 'General' do = link_to edit_group_path(@group), title: 'General' do
%span %span

View File

@ -12,12 +12,22 @@
= custom_icon('profile') = custom_icon('profile')
%span.nav-item-name %span.nav-item-name
Profile 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 = nav_link(controller: [:accounts, :two_factor_auths]) do
= sidebar_link profile_account_path, title: _('Account') do = sidebar_link profile_account_path, title: _('Account') do
.nav-icon-container .nav-icon-container
= custom_icon('account') = custom_icon('account')
%span.nav-item-name %span.nav-item-name
Account 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? - if current_application_settings.user_oauth_applications?
= nav_link(controller: 'oauth/applications') do = nav_link(controller: 'oauth/applications') do
= sidebar_link applications_profile_path, title: _('Applications') do = sidebar_link applications_profile_path, title: _('Applications') do
@ -25,24 +35,44 @@
= custom_icon('applications') = custom_icon('applications')
%span.nav-item-name %span.nav-item-name
Applications 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 = nav_link(controller: :chat_names) do
= sidebar_link profile_chat_names_path, title: _('Chat') do = sidebar_link profile_chat_names_path, title: _('Chat') do
.nav-icon-container .nav-icon-container
= custom_icon('chat') = custom_icon('chat')
%span.nav-item-name %span.nav-item-name
Chat 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 = nav_link(controller: :personal_access_tokens) do
= sidebar_link profile_personal_access_tokens_path, title: _('Access Tokens') do = sidebar_link profile_personal_access_tokens_path, title: _('Access Tokens') do
.nav-icon-container .nav-icon-container
= custom_icon('access_tokens') = custom_icon('access_tokens')
%span.nav-item-name %span.nav-item-name
Access Tokens 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 = nav_link(controller: :emails) do
= sidebar_link profile_emails_path, title: _('Emails') do = sidebar_link profile_emails_path, title: _('Emails') do
.nav-icon-container .nav-icon-container
= custom_icon('emails') = custom_icon('emails')
%span.nav-item-name %span.nav-item-name
Emails 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? - unless current_user.ldap_user?
= nav_link(controller: :passwords) do = nav_link(controller: :passwords) do
= sidebar_link edit_profile_password_path, title: _('Password') do = sidebar_link edit_profile_password_path, title: _('Password') do
@ -50,36 +80,65 @@
= custom_icon('lock') = custom_icon('lock')
%span.nav-item-name %span.nav-item-name
Password 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 = nav_link(controller: :notifications) do
= sidebar_link profile_notifications_path, title: _('Notifications') do = sidebar_link profile_notifications_path, title: _('Notifications') do
.nav-icon-container .nav-icon-container
= custom_icon('notifications') = custom_icon('notifications')
%span.nav-item-name %span.nav-item-name
Notifications 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 = nav_link(controller: :keys) do
= sidebar_link profile_keys_path, title: _('SSH Keys') do = sidebar_link profile_keys_path, title: _('SSH Keys') do
.nav-icon-container .nav-icon-container
= custom_icon('key') = custom_icon('key')
%span.nav-item-name %span.nav-item-name
SSH Keys 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 = nav_link(controller: :gpg_keys) do
= sidebar_link profile_gpg_keys_path, title: _('GPG Keys') do = sidebar_link profile_gpg_keys_path, title: _('GPG Keys') do
.nav-icon-container .nav-icon-container
= custom_icon('key_2') = custom_icon('key_2')
%span.nav-item-name %span.nav-item-name
GPG Keys 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 = nav_link(controller: :preferences) do
= sidebar_link profile_preferences_path, title: _('Preferences') do = sidebar_link profile_preferences_path, title: _('Preferences') do
.nav-icon-container .nav-icon-container
= custom_icon('preferences') = custom_icon('preferences')
%span.nav-item-name %span.nav-item-name
Preferences 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 = nav_link(path: 'profiles#audit_log') do
= sidebar_link audit_log_profile_path, title: _('Authentication log') do = sidebar_link audit_log_profile_path, title: _('Authentication log') do
.nav-icon-container .nav-icon-container
= custom_icon('authentication_log') = custom_icon('authentication_log')
%span.nav-item-name %span.nav-item-name
Authentication log 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' = render 'shared/sidebar_toggle_button'

View File

@ -16,6 +16,11 @@
Overview Overview
%ul.sidebar-sub-level-items %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 = nav_link(path: 'projects#show') do
= link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do
%span= _('Details') %span= _('Details')
@ -38,6 +43,11 @@
Repository Repository
%ul.sidebar-sub-level-items %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 = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
= link_to project_tree_path(@project) do = link_to project_tree_path(@project) do
#{ _('Files') } #{ _('Files') }
@ -90,6 +100,14 @@
= number_with_delimiter(@project.open_issues_count) = number_with_delimiter(@project.open_issues_count)
%ul.sidebar-sub-level-items %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 = nav_link(controller: :issues) do
= link_to project_issues_path(@project), title: 'Issues' do = link_to project_issues_path(@project), title: 'Issues' do
%span %span
@ -133,6 +151,13 @@
Merge Requests Merge Requests
%span.badge.count.merge_counter.js-merge-counter %span.badge.count.merge_counter.js-merge-counter
= number_with_delimiter(@project.open_merge_requests_count) = 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 - if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do
@ -143,6 +168,11 @@
CI / CD CI / CD
%ul.sidebar-sub-level-items %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 - if project_nav_tab? :pipelines
= nav_link(path: ['pipelines#index', 'pipelines#show']) do = nav_link(path: ['pipelines#index', 'pipelines#show']) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
@ -180,6 +210,11 @@
= custom_icon('wiki') = custom_icon('wiki')
%span.nav-item-name %span.nav-item-name
Wiki 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 - if project_nav_tab? :snippets
= nav_link(controller: :snippets) do = nav_link(controller: :snippets) do
@ -188,6 +223,11 @@
= custom_icon('snippets') = custom_icon('snippets')
%span.nav-item-name %span.nav-item-name
Snippets 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 - 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 = 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 %ul.sidebar-sub-level-items
- can_edit = can?(current_user, :admin_project, @project) - can_edit = can?(current_user, :admin_project, @project)
- if can_edit - 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 = nav_link(path: %w[projects#edit]) do
= link_to edit_project_path(@project), title: 'General' do = link_to edit_project_path(@project), title: 'General' do
%span %span

View File

@ -3,6 +3,26 @@
= render 'profiles/head' = 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| = 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 .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
Syntax highlighting theme Syntax highlighting theme

View File

@ -1,3 +1,7 @@
// 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 // Toggle container-fluid class
if ('<%= current_user.layout %>' === 'fluid') { if ('<%= current_user.layout %>' === 'fluid') {
$('.content-wrapper .container-fluid').removeClass('container-limited') $('.content-wrapper .container-fluid').removeClass('container-limited')

View File

@ -1 +1 @@
= render "show" = render "shared/boards/show", board: @boards.first

View File

@ -1 +1 @@
= render "show" = render "shared/boards/show", board: @board

View File

@ -28,8 +28,8 @@
= link_to icon('question-circle'), help_page_path("gitlab-basics/create-project"), target: '_blank', aria: { label: "Whats included in a template?" }, title: "Whats included in a template?", class: 'has-tooltip', data: { placement: 'top'} = link_to icon('question-circle'), help_page_path("gitlab-basics/create-project"), target: '_blank', aria: { label: "Whats included in a template?" }, title: "Whats included in a template?", class: 'has-tooltip', data: { placement: 'top'}
%div %div
= render 'project_templates', f: f = render 'project_templates', f: f
.second-column - if import_sources_enabled?
- if import_sources_enabled? .second-column
.project-import .project-import
.form-group.clearfix .form-group.clearfix
= f.label :visibility_level, class: 'label-light' do #the label here seems wrong = f.label :visibility_level, class: 'label-light' do #the label here seems wrong

View File

@ -27,9 +27,10 @@
= link_to project_tags_path(@project) do = link_to project_tags_path(@project) do
#{n_('Tag', 'Tags', @repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)}) #{n_('Tag', 'Tags', @repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)})
- if default_project_view != 'readme' && @repository.readme - if @repository.readme
%li %li
= link_to _('Readme'), readme_path(@project) = link_to _('Readme'),
default_project_view != 'readme' ? readme_path(@project) : '#readme'
- if @repository.changelog - if @repository.changelog
%li %li

View File

@ -1,5 +1,5 @@
- if readme.rich_viewer - 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 .js-file-title.file-title
= blob_icon readme.mode, readme.name = blob_icon readme.mode, readme.name
= link_to project_blob_path(@project, tree_join(@ref, readme.path)) do = link_to project_blob_path(@project, tree_join(@ref, readme.path)) do

View File

@ -9,7 +9,7 @@
= webpack_bundle_tag 'filtered_search' = webpack_bundle_tag 'filtered_search'
= webpack_bundle_tag 'boards' = 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 %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
= render "projects/issues/head" = render "projects/issues/head"
@ -30,7 +30,7 @@
":root-path" => "rootPath", ":root-path" => "rootPath",
":board-id" => "boardId", ":board-id" => "boardId",
":key" => "_uid" } ":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'), %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'),
"new-issue-path" => new_project_issue_path(@project), "new-issue-path" => new_project_issue_path(@project),
"milestone-path" => milestones_filter_dropdown_path, "milestone-path" => milestones_filter_dropdown_path,

View File

@ -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 }", ":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" } "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 : "")' } ":title" => '(list.label ? list.label.description : "")' }
{{ list.title }} {{ list.title }}
%span.has-tooltip{ "v-if": "list.type === \"label\"", %span.has-tooltip{ "v-if": "list.type === \"label\"",
":title" => '(list.label ? list.label.description : "")', ":title" => '(list.label ? list.label.description : "")',
data: { container: "body", placement: "bottom" }, 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\") }" } ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.color ? list.label.text_color : \"#2e2e2e\") }" }
{{ list.title }} {{ 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 }' } %span.issue-count-badge-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
{{ list.issuesSize }} {{ 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", %button.issue-count-badge-add-button.btn.btn-small.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button",
"@click" => "showNewIssueForm", "@click" => "showNewIssueForm",
"v-if" => 'list.type !== "closed"', "v-if" => 'list.type !== "closed"',
@ -28,12 +34,7 @@
"title" => "New issue", "title" => "New issue",
data: { placement: "top", container: "body" } } data: { placement: "top", container: "body" } }
= icon("plus", class: "js-no-trigger-collapse") = 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"', %board-list{ "v-if" => 'list.type !== "blank"',
":list" => "list", ":list" => "list",
":issues" => "list.issues", ":issues" => "list.issues",
@ -42,5 +43,5 @@
":issue-link-base" => "issueLinkBase", ":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath", ":root-path" => "rootPath",
"ref" => "board-list" } "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"' } %board-blank-state{ "v-if" => 'list.id == "blank"' }

View File

@ -10,18 +10,19 @@
%br/ %br/
%span %span
= precede "#" do = precede "#" do
{{ issue.id }} {{ issue.iid }}
%a.gutter-toggle.pull-right{ role: "button", %a.gutter-toggle.pull-right{ role: "button",
href: "#", href: "#",
"@click.prevent" => "closeSidebar", "@click.prevent" => "closeSidebar",
"aria-label" => "Toggle sidebar" } "aria-label" => "Toggle sidebar" }
= custom_icon("icon_close", size: 15) = custom_icon("icon_close", size: 15)
.js-issuable-update .js-issuable-update
= render "projects/boards/components/sidebar/assignee" = render "shared/boards/components/sidebar/assignee"
= render "projects/boards/components/sidebar/milestone" = render "shared/boards/components/sidebar/milestone"
= render "projects/boards/components/sidebar/due_date" = render "shared/boards/components/sidebar/due_date"
= render "projects/boards/components/sidebar/labels" = render "shared/boards/components/sidebar/labels"
= render "projects/boards/components/sidebar/notifications" = render "shared/boards/components/sidebar/notifications"
%remove-btn{ ":issue" => "issue", %remove-btn{ ":issue" => "issue",
":issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'",
":list" => "list", ":list" => "list",
"v-if" => "canRemove" } "v-if" => "canRemove" }

View File

@ -2,13 +2,13 @@
%template{ "v-if" => "issue.assignees" } %template{ "v-if" => "issue.assignees" }
%assignee-title{ ":number-of-assignees" => "issue.assignees.length", %assignee-title{ ":number-of-assignees" => "issue.assignees.length",
":loading" => "loadingAssignees", ":loading" => "loadingAssignees",
":editable" => can?(current_user, :admin_issue, @project) } ":editable" => can_admin_issue? }
%assignees.value{ "root-path" => "#{root_url}", %assignees.value{ "root-path" => "#{root_url}",
":users" => "issue.assignees", ":users" => "issue.assignees",
":editable" => can?(current_user, :admin_issue, @project), ":editable" => can_admin_issue?,
"@assign-self" => "assignSelf" } "@assign-self" => "assignSelf" }
- if can?(current_user, :admin_issue, @project) - if can_admin_issue?
.selectbox.hide-collapsed .selectbox.hide-collapsed
%input.js-vue{ type: "hidden", %input.js-vue{ type: "hidden",
name: "issue[assignee_ids][]", name: "issue[assignee_ids][]",
@ -20,9 +20,9 @@
":data-username" => "assignee.username" } ":data-username" => "assignee.username" }
.dropdown .dropdown
- dropdown_options = issue_assignees_dropdown_options - 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'] }, %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.id", ":data-issuable-id" => "issue.iid",
":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" } ":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
= dropdown_options[:title] = dropdown_options[:title]
= icon("chevron-down") = icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author .dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author

View File

@ -1,7 +1,7 @@
.block.due_date .block.due_date
.title .title
Due date Due date
- if can?(current_user, :admin_issue, @project) - if can_admin_issue?
= icon("spinner spin", class: "block-loading") = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right" = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
.value .value
@ -10,12 +10,12 @@
No due date No due date
%span.bold{ "v-if" => "issue.dueDate" } %span.bold{ "v-if" => "issue.dueDate" }
{{ issue.dueDate | due-date }} {{ 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" } %span.no-value.js-remove-due-date-holder{ "v-if" => "issue.dueDate" }
\- \-
%a.js-remove-due-date{ href: "#", role: "button" } %a.js-remove-due-date{ href: "#", role: "button" }
remove due date remove due date
- if can?(current_user, :admin_issue, @project) - if can_admin_issue?
.selectbox .selectbox
%input{ type: "hidden", %input{ type: "hidden",
name: "issue[due_date]", name: "issue[due_date]",
@ -23,7 +23,7 @@
.dropdown .dropdown
%button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button', %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: { 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 %span.dropdown-toggle-text Due date
= icon('chevron-down') = icon('chevron-down')
.dropdown-menu.dropdown-menu-due-date .dropdown-menu.dropdown-menu-due-date

View File

@ -1,7 +1,7 @@
.block.labels .block.labels
.title .title
Labels Labels
- if can?(current_user, :admin_issue, @project) - if can_admin_issue?
= icon("spinner spin", class: "block-loading") = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right" = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
.value.issuable-show-labels .value.issuable-show-labels
@ -11,7 +11,7 @@
"v-for" => "label in issue.labels" } "v-for" => "label in issue.labels" }
%span.label.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" } %span.label.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
{{ label.title }} {{ label.title }}
- if can?(current_user, :admin_issue, @project) - if can_admin_issue?
.selectbox .selectbox
%input{ type: "hidden", %input{ type: "hidden",
name: "issue[label_names][]", name: "issue[label_names][]",
@ -19,12 +19,19 @@
":value" => "label.id" } ":value" => "label.id" }
.dropdown .dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button", %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: { toggle: "dropdown",
":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" } 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 %span.dropdown-toggle-text
Label Label
= icon('chevron-down') = icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default" = 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" = render partial: "shared/issuable/label_page_create"

View File

@ -1,7 +1,7 @@
.block.milestone .block.milestone
.title .title
Milestone Milestone
- if can?(current_user, :admin_issue, @project) - if can_admin_issue?
= icon("spinner spin", class: "block-loading") = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right" = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
.value .value
@ -9,17 +9,17 @@
None None
%span.bold.has-tooltip{ "v-if" => "issue.milestone" } %span.bold.has-tooltip{ "v-if" => "issue.milestone" }
{{ issue.milestone.title }} {{ issue.milestone.title }}
- if can?(current_user, :admin_issue, @project) - if can_admin_issue?
.selectbox .selectbox
%input{ type: "hidden", %input{ type: "hidden",
":value" => "issue.milestone.id", ":value" => "issue.milestone.id",
name: "issue[milestone_id]", name: "issue[milestone_id]",
"v-if" => "issue.milestone" } "v-if" => "issue.milestone" }
.dropdown .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-selected" => "milestoneTitle",
":data-issuable-id" => "issue.id", ":data-issuable-id" => "issue.iid",
":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" } ":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
Milestone Milestone
= icon("chevron-down") = icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-menu.dropdown-select.dropdown-menu-selectable

View File

@ -1,5 +1,5 @@
- if current_user - 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 %span.issuable-header-text.hide-collapsed.pull-left
Notifications Notifications
%button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }

View File

@ -0,0 +1 @@
= render "show"

View File

@ -0,0 +1 @@
= render "show"

View File

@ -8,20 +8,19 @@
- if show_boards_content - if show_boards_content
.issue-board-dropdown-content .issue-board-dropdown-content
%p %p
Create lists from the labels you use in your project. Issues with that Create lists from labels. Issues with that label appear in that list.
label will automatically be added to the list.
= dropdown_filter(filter_placeholder) = dropdown_filter(filter_placeholder)
= dropdown_content = dropdown_content
- if @project && show_footer - if current_board_parent && show_footer
= dropdown_footer do = dropdown_footer do
%ul.dropdown-footer-list %ul.dropdown-footer-list
- if can?(current_user, :admin_label, @project) - if can?(current_user, :admin_label, current_board_parent)
%li %li
%a.dropdown-toggle-page{ href: "#" } %a.dropdown-toggle-page{ href: "#" }
Create new label Create new label
%li %li
= link_to project_labels_path(@project), :"data-is-link" => true do = link_to labels_path, :"data-is-link" => true do
- if show_create && @project && can?(current_user, :admin_label, @project) - if show_create && can?(current_user, :admin_label, current_board_parent)
Manage labels Manage labels
- else - else
View labels View labels

View File

@ -104,13 +104,13 @@
= icon('times') = icon('times')
.filter-dropdown-container .filter-dropdown-container
- if type == :boards - 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 .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 Add list
.dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable .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" } = 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" = render partial: "shared/issuable/label_page_create"
= dropdown_loading = dropdown_loading
#js-add-issues-btn.prepend-left-10 #js-add-issues-btn.prepend-left-10

View File

@ -0,0 +1,5 @@
---
title: Add div id to the readme in the project overview
merge_request: 13735
author: Riccardo Padovani @rpadovani
type: added

View File

@ -0,0 +1,5 @@
---
title: Add option in preferences to change navigation theme color
merge_request:
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Finish migration to the new events setup
merge_request:
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add documentation for PlantUML in reStructuredText
merge_request: 13900
author: Markus Koller
type: other

View File

@ -0,0 +1,5 @@
---
title: Fix stray OR in New Project page
merge_request: 14096
author: Robin Bobbitt
type: fixed

View File

@ -76,6 +76,13 @@ production: &base
# default_can_create_group: false # default: true # default_can_create_group: false # default: true
# username_changing_enabled: false # default: true - User can change her username/namespace # 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 ## Automatic issue closing
# If a commit message matches this regular expression, all issues referenced from the matched text will be closed. # If a commit message matches this regular expression, all issues referenced from the matched text will be closed.

View File

@ -232,6 +232,7 @@ Settings['gitlab'] ||= Settingslogic.new({})
Settings.gitlab['default_projects_limit'] ||= 100000 Settings.gitlab['default_projects_limit'] ||= 100000
Settings.gitlab['default_branch_protection'] ||= 2 Settings.gitlab['default_branch_protection'] ||= 2
Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil? 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['host'] ||= ENV['GITLAB_HOST'] || 'localhost'
Settings.gitlab['ssh_host'] ||= Settings.gitlab.host Settings.gitlab['ssh_host'] ||= Settings.gitlab.host
Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? Settings.gitlab['https'] = false if Settings.gitlab['https'].nil?

View File

@ -74,6 +74,19 @@ Rails.application.routes.draw do
# Notification settings # Notification settings
resources :notification_settings, only: [:create, :update] 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 :import
draw :uploads draw :uploads
draw :explore draw :explore

View File

@ -343,19 +343,7 @@ constraints(ProjectUrlConstrainer.new) do
get 'noteable/:target_type/:target_id/notes' => 'notes#index', as: 'noteable_notes' get 'noteable/:target_type/:target_id/notes' => 'notes#index', as: 'noteable_notes'
resources :boards, only: [:index, :show] do resources :boards, only: [:index, :show, :create, :update, :destroy]
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 :todos, only: [:create] resources :todos, only: [:create]

View File

@ -0,0 +1,10 @@
# 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

View File

@ -0,0 +1,18 @@
# 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

View File

@ -0,0 +1,23 @@
# 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

View File

@ -7,6 +7,5 @@ class LimitsToMysql < ActiveRecord::Migration
change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647 change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647
change_column :snippets, :content, :text, limit: 2147483647 change_column :snippets, :content, :text, limit: 2147483647
change_column :notes, :st_diff, :text, limit: 2147483647 change_column :notes, :st_diff, :text, limit: 2147483647
change_column :events, :data, :text, limit: 2147483647
end end
end end

Some files were not shown because too many files have changed in this diff Show More