summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml12
-rw-r--r--CHANGELOG.md45
-rw-r--r--app/assets/javascripts/awards_handler.js2
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es613
-rw-r--r--app/assets/javascripts/boards/components/board.js.es621
-rw-r--r--app/assets/javascripts/boards/components/board_card.js.es634
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js.es67
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js.es652
-rw-r--r--app/assets/javascripts/boards/filters/due_date_filters.js.es64
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js.es62
-rw-r--r--app/assets/javascripts/boards/models/issue.js.es623
-rw-r--r--app/assets/javascripts/boards/models/milestone.js.es66
-rw-r--r--app/assets/javascripts/boards/services/board_service.js.es62
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js.es63
-rw-r--r--app/assets/javascripts/due_date_select.js.es629
-rw-r--r--app/assets/javascripts/gl_dropdown.js29
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js11
-rw-r--r--app/assets/javascripts/issuable_form.js4
-rw-r--r--app/assets/javascripts/labels_select.js29
-rw-r--r--app/assets/javascripts/milestone_select.js21
-rw-r--r--app/assets/javascripts/project.js2
-rw-r--r--app/assets/javascripts/right_sidebar.js11
-rw-r--r--app/assets/javascripts/subscription.js32
-rw-r--r--app/assets/javascripts/users_select.js45
-rw-r--r--app/assets/stylesheets/framework/header.scss2
-rw-r--r--app/assets/stylesheets/pages/boards.scss66
-rw-r--r--app/controllers/projects/boards/issues_controller.rb9
-rw-r--r--app/helpers/sidekiq_helper.rb2
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/views/help/ui.html.haml2
-rw-r--r--app/views/projects/boards/components/_card.html.haml17
-rw-r--r--app/views/projects/boards/components/_sidebar.html.haml23
-rw-r--r--app/views/projects/boards/components/sidebar/_assignee.html.haml40
-rw-r--r--app/views/projects/boards/components/sidebar/_due_date.html.haml32
-rw-r--r--app/views/projects/boards/components/sidebar/_labels.html.haml30
-rw-r--r--app/views/projects/boards/components/sidebar/_milestone.html.haml28
-rw-r--r--app/views/projects/boards/components/sidebar/_notifications.html.haml11
-rw-r--r--app/views/projects/boards/index.html.haml10
-rw-r--r--app/views/projects/boards/show.html.haml10
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml2
-rw-r--r--app/views/shared/icons/_icon_close.svg1
-rw-r--r--doc/api/services.md46
-rw-r--r--doc/install/installation.md3
-rw-r--r--lib/api/deploy_keys.rb9
-rw-r--r--lib/api/tags.rb94
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb4
-rw-r--r--spec/features/boards/new_issue_spec.rb15
-rw-r--r--spec/features/boards/sidebar_spec.rb312
-rw-r--r--spec/features/issues/filter_by_milestone_spec.rb16
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb17
-rw-r--r--spec/fixtures/api/schemas/issue.json4
-rw-r--r--spec/requests/api/deploy_keys_spec.rb17
52 files changed, 1108 insertions, 157 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0b27110d5cc..3f315550536 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -215,6 +215,7 @@ rake ee_compat_check:
only:
- branches
except:
+ - master
- tags
allow_failure: yes
@@ -319,15 +320,20 @@ lint-javascript:
- npm run eslint
# Trigger docs build
+# https://gitlab.com/gitlab-com/doc-gitlab-com/blob/master/README.md#deployment-process
trigger_docs:
stage: post-test
- before_script: []
+ image: "alpine"
+ before_script:
+ - apk update && apk add curl
+ variables:
+ GIT_STRATEGY: none
cache: {}
artifacts: {}
script:
- - "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master https://gitlab.com/api/v3/projects/38069/trigger/builds"
+ - "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=ce https://gitlab.com/api/v3/projects/38069/trigger/builds"
only:
- - master
+ - master@gitlab-org/gitlab-ce
# Notify slack in the end
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 92c730eaa8a..a36a6652ec8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,31 +8,45 @@ Please view this file on the master branch, on stable branches it's out of date.
- Fix extra space on Build sidebar on Firefox !7060
- Fix HipChat notifications rendering (airatshigapov, eisnerd)
- Add hover to trash icon in notes !7008 (blackst0ne)
+ - Fix sidekiq stats in admin area (blackst0ne)
- Escape ref and path for relative links !6050 (winniehell)
+ - Fixed link typo on /help/ui to Alerts section. !6915 (Sam Rose)
+ - Fix filtering of milestones with quotes in title (airatshigapov)
+ - Refactor less readable existance checking code from CoffeeScript !6289 (jlogandavison)
- Simpler arguments passed to named_route on toggle_award_url helper method
+ - Fix typo in framework css class. !7086 (Daniel Voogsgerd)
- Fix: Backup restore doesn't clear cache
+ - API: Fix project deploy keys 400 and 500 errors when adding an existing key. !6784 (Joshua Welsh)
- Replace jquery.cookie plugin with js.cookie !7085
- Use MergeRequestsClosingIssues cache data on Issue#closed_by_merge_requests method
- Fix Sign in page 'Forgot your password?' link overlaps on medium-large screens
- Fix documents and comments on Build API `scope`
- Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov)
-## 8.13.1 (unreleased)
- - Fix bug where labels would be assigned to issues that were moved
- - Fix error in generating labels
- - Fix reply-by-email not working due to queue name mismatch
- - Fixed hidden pipeline graph on commit and MR page !6895
- - Expire and build repository cache after project import
- - Reduce overhead of LabelFinder by avoiding #presence call !7094
- - Fix 404 for group pages when GitLab setup uses relative url
- - Simpler arguments passed to named_route on toggle_award_url helper method
- - Fix unauthorized users dragging on issue boards
- - Better handle when no users were selected for adding to group or project. (Linus Thiel)
- - Only show register tab if signup enabled.
- - Only schedule ProjectCacheWorker jobs when needed
+## 8.13.1 (2016-10-25)
+ - Fix branch protection API. !6215
+ - Fix hidden pipeline graph on commit and MR page. !6895
+ - Fix Cycle analytics not showing correct data when filtering by date. !6906
+ - Ensure custom provider tab labels don't break layout. !6993
+ - Fix issue boards user link when in subdirectory. !7018
+ - Refactor and add new environment functionality to CI yaml reference. !7026
+ - Fix typo in project settings that prevents users from enabling container registry. !7037
+ - Fix events order in `users/:id/events` endpoint. !7039
+ - Remove extra line for empty issue description. !7045
+ - Don't append issue/MR templates to any existing text. !7050
+ - Fix error in generating labels. !7055
+ - Stop clearing the database cache on `rake cache:clear`. !7056
+ - Only show register tab if signup enabled. !7058
+ - Expire and build repository cache after project import. !7064
+ - Fix bug where labels would be assigned to issues that were moved. !7065
+ - Fix reply-by-email not working due to queue name mismatch. !7068
+ - Fix 404 for group pages when GitLab setup uses relative url. !7071
+ - Fix `User#to_reference`. !7088
+ - Reduce overhead of `LabelFinder` by avoiding `#presence` call. !7094
+ - Fix unauthorized users dragging on issue boards. !7096
+ - Only schedule `ProjectCacheWorker` jobs when needed. !7099
## 8.13.0 (2016-10-22)
- - Removes extra line for empty issue description. (!7045)
- Fix save button on project pipeline settings page. (!6955)
- All Sidekiq workers now use their own queue
- Avoid race condition when asynchronously removing expired artifacts. (!6881)
@@ -53,7 +67,6 @@ Please view this file on the master branch, on stable branches it's out of date.
- Update duration at the end of pipeline
- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
- Add group level labels. (!6425)
- - Fix Cycle analytics not showing correct data when filtering by date. !6906
- Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun)
- Cancelled pipelines could be retried. !6927
- Updating verbiage on git basics to be more intuitive
@@ -61,7 +74,6 @@ Please view this file on the master branch, on stable branches it's out of date.
- Clarify documentation for Runners API (Gennady Trafimenkov)
- The instrumentation for Banzai::Renderer has been restored
- Change user & group landing page routing from /u/:username to /:username
- - Fixed issue boards user link when in subdirectory
- Added documentation for .gitattributes files
- Move Pipeline Metrics to separate worker
- AbstractReferenceFilter caches project_refs on RequestStore when active
@@ -408,7 +420,6 @@ Please view this file on the master branch, on stable branches it's out of date.
- Fix inconsistent checkbox alignment (ClemMakesApps)
- Use the default branch for displaying the project icon instead of master !5792 (Hannes Rosenögger)
- Adds response mime type to transaction metric action when it's not HTML
- - Fix branch protection API !6215
- Fix hover leading space bug in pipeline graph !5980
- Avoid conflict with admin labels when importing GitHub labels
- User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 9adf2bd6c61..8bdb0965f99 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -92,7 +92,7 @@
css = {
top: ($addBtn.offset().top + $addBtn.outerHeight()) + "px"
};
- if ((position != null) && position === 'right') {
+ if (position === 'right') {
css.left = (($addBtn.offset().left - $menu.outerWidth()) + 20) + "px";
$menu.addClass('is-aligned-right');
} else {
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index 9f435ff936a..efb22d38513 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -6,7 +6,9 @@
//= require_tree ./stores
//= require_tree ./services
//= require_tree ./mixins
+//= require_tree ./filters
//= require ./components/board
+//= require ./components/board_sidebar
//= require ./components/new_list_dropdown
//= require ./vue_resource_interceptor
@@ -23,7 +25,8 @@ $(() => {
gl.IssueBoardsApp = new Vue({
el: $boardApp,
components: {
- 'board': gl.issueBoards.Board
+ 'board': gl.issueBoards.Board,
+ 'board-sidebar': gl.issueBoards.BoardSidebar
},
data: {
state: Store.state,
@@ -31,9 +34,15 @@ $(() => {
endpoint: $boardApp.dataset.endpoint,
boardId: $boardApp.dataset.boardId,
disabled: $boardApp.dataset.disabled === 'true',
- issueLinkBase: $boardApp.dataset.issueLinkBase
+ issueLinkBase: $boardApp.dataset.issueLinkBase,
+ detailIssue: Store.detail
},
init: Store.create.bind(Store),
+ computed: {
+ detailIssueVisible () {
+ return Object.keys(this.detailIssue.issue).length;
+ }
+ },
created () {
gl.boardService = new BoardService(this.endpoint, this.boardId);
},
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
index 8e57a935877..0e03d43872b 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -22,6 +22,7 @@
},
data () {
return {
+ detailIssue: Store.detail,
filters: Store.state.filters,
showIssueForm: false
};
@@ -33,6 +34,26 @@
this.list.getIssues(true);
},
deep: true
+ },
+ detailIssue: {
+ handler () {
+ if (!Object.keys(this.detailIssue.issue).length) return;
+
+ const issue = this.list.findIssue(this.detailIssue.issue.id);
+
+ if (issue) {
+ const boardsList = document.querySelectorAll('.boards-list')[0];
+ const right = (this.$el.offsetLeft + this.$el.offsetWidth) - boardsList.offsetWidth;
+ const left = boardsList.scrollLeft - this.$el.offsetLeft;
+
+ if (right - boardsList.scrollLeft > 0) {
+ boardsList.scrollLeft = right;
+ } else if (left > 0) {
+ boardsList.scrollLeft = this.$el.offsetLeft;
+ }
+ }
+ },
+ deep: true
}
},
methods: {
diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6
index 35f675fda62..2f6c03e3538 100644
--- a/app/assets/javascripts/boards/components/board_card.js.es6
+++ b/app/assets/javascripts/boards/components/board_card.js.es6
@@ -13,6 +13,17 @@
disabled: Boolean,
index: Number
},
+ data () {
+ return {
+ showDetail: false,
+ detailIssue: Store.detail
+ };
+ },
+ computed: {
+ issueDetailVisible () {
+ return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
+ }
+ },
methods: {
filterByLabel (label, e) {
let labelToggleText = label.title;
@@ -38,6 +49,29 @@
$('.labels-filter .dropdown-toggle-text').text(labelToggleText);
Store.updateFiltersUrl();
+ },
+ mouseDown () {
+ this.showDetail = true;
+ },
+ mouseMove () {
+ if (this.showDetail) {
+ this.showDetail = false;
+ }
+ },
+ showIssue (e) {
+ const targetTagName = e.target.tagName.toLowerCase();
+
+ if (targetTagName === 'a' || targetTagName === 'button') return;
+
+ if (this.showDetail) {
+ this.showDetail = false;
+
+ if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
+ Store.detail.issue = {};
+ } else {
+ Store.detail.issue = this.issue;
+ }
+ }
}
}
});
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6
index 2c784b6c53a..7fc0bfd56f3 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.js.es6
+++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6
@@ -1,5 +1,7 @@
/* eslint-disable */
(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
window.gl = window.gl || {};
gl.issueBoards.BoardNewIssue = Vue.extend({
@@ -28,13 +30,16 @@
const labels = this.list.label ? [this.list.label] : [];
const issue = new ListIssue({
title: this.title,
- labels
+ labels,
+ subscribed: true
});
this.list.newIssue(issue)
.then((data) => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$els.submitButton).enable();
+
+ Store.detail.issue = issue;
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6
new file mode 100644
index 00000000000..e83e69247d9
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6
@@ -0,0 +1,52 @@
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardSidebar = Vue.extend({
+ props: {
+ currentUser: Object
+ },
+ data() {
+ return {
+ detail: Store.detail,
+ issue: {}
+ };
+ },
+ computed: {
+ showSidebar () {
+ return Object.keys(this.issue).length;
+ }
+ },
+ watch: {
+ detail: {
+ handler () {
+ this.issue = this.detail.issue;
+ },
+ deep: true
+ },
+ issue () {
+ if (this.showSidebar) {
+ this.$nextTick(() => {
+ $('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0);
+ $('.right-sidebar').getNiceScroll().resize();
+ });
+ }
+ }
+ },
+ methods: {
+ closeSidebar () {
+ this.detail.issue = {};
+ }
+ },
+ ready () {
+ new IssuableContext(this.currentUser);
+ new MilestoneSelect();
+ new gl.DueDateSelectors();
+ new LabelsSelect();
+ new Sidebar();
+ new Subscription('.subscription');
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
new file mode 100644
index 00000000000..50ef1911022
--- /dev/null
+++ b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
@@ -0,0 +1,4 @@
+Vue.filter('due-date', (value) => {
+ const date = new Date(value);
+ return $.datepicker.formatDate('M d, yy', date);
+});
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
index 1ffd0ad2f22..e520170ef74 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -23,7 +23,7 @@
fallbackOnBody: true,
ghostClass: 'is-ghost',
filter: '.has-tooltip, .btn',
- delay: gl.issueBoards.touchEnabled ? 100 : 0,
+ delay: gl.issueBoards.touchEnabled ? 100 : 50,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
onStart: gl.issueBoards.onStart,
diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6
index 98ad62b37d3..21d735e8231 100644
--- a/app/assets/javascripts/boards/models/issue.js.es6
+++ b/app/assets/javascripts/boards/models/issue.js.es6
@@ -4,12 +4,18 @@ class ListIssue {
this.id = obj.iid;
this.title = obj.title;
this.confidential = obj.confidential;
+ this.dueDate = obj.due_date;
+ this.subscribed = obj.subscribed;
this.labels = [];
if (obj.assignee) {
this.assignee = new ListUser(obj.assignee);
}
+ if (obj.milestone) {
+ this.milestone = new ListMilestone(obj.milestone);
+ }
+
obj.labels.forEach((label) => {
this.labels.push(new ListLabel(label));
});
@@ -42,4 +48,21 @@ class ListIssue {
getLists () {
return gl.issueBoards.BoardsStore.state.lists.filter( list => list.findIssue(this.id) );
}
+
+ update (url) {
+ const data = {
+ issue: {
+ milestone_id: this.milestone ? this.milestone.id : null,
+ due_date: this.dueDate,
+ assignee_id: this.assignee ? this.assignee.id : null,
+ label_ids: this.labels.map( (label) => label.id )
+ }
+ };
+
+ if (!data.issue.label_ids.length) {
+ data.issue.label_ids = [''];
+ }
+
+ return Vue.http.patch(url, data);
+ }
}
diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js.es6
new file mode 100644
index 00000000000..577adf11265
--- /dev/null
+++ b/app/assets/javascripts/boards/models/milestone.js.es6
@@ -0,0 +1,6 @@
+class ListMilestone {
+ constructor (obj) {
+ this.id = obj.id;
+ this.title = obj.title;
+ }
+}
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
index be37501e6f4..f59a2ed7937 100644
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -1,8 +1,6 @@
/* eslint-disable */
class BoardService {
constructor (root, boardId) {
- Vue.http.options.root = root;
-
this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
generate: {
method: 'POST',
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
index 8d98068020b..534845cd8a2 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js.es6
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -6,6 +6,9 @@
gl.issueBoards.BoardsStore = {
disabled: false,
state: {},
+ detail: {
+ issue: {}
+ },
moving: {
issue: {},
list: {}
diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6
index 1c30b81c94b..fd7f961aab9 100644
--- a/app/assets/javascripts/due_date_select.js.es6
+++ b/app/assets/javascripts/due_date_select.js.es6
@@ -42,7 +42,12 @@
defaultDate: $("input[name='" + this.fieldName + "']").val(),
altField: "input[name='" + this.fieldName + "']",
onSelect: () => {
- return this.saveDueDate(true);
+ if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
+ gl.issueBoards.BoardsStore.detail.issue.dueDate = $(`input[name='${this.fieldName}']`).val();
+ this.updateIssueBoardIssue();
+ } else {
+ return this.saveDueDate(true);
+ }
}
});
}
@@ -50,8 +55,14 @@
initRemoveDueDate() {
this.$block.on('click', '.js-remove-due-date', (e) => {
e.preventDefault();
- $("input[name='" + this.fieldName + "']").val('');
- return this.saveDueDate(false);
+
+ if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
+ gl.issueBoards.BoardsStore.detail.issue.dueDate = '';
+ this.updateIssueBoardIssue();
+ } else {
+ $("input[name='" + this.fieldName + "']").val('');
+ return this.saveDueDate(false);
+ }
});
}
@@ -84,6 +95,18 @@
this.datePayload = datePayload;
}
+ updateIssueBoardIssue () {
+ this.$loading.fadeIn();
+ this.$dropdown.trigger('loading.gl.dropdown');
+ this.$selectbox.hide();
+ this.$value.css('display', '');
+
+ gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update'))
+ .then(() => {
+ this.$loading.fadeOut();
+ });
+ }
+
submitSelectedDate(isDropdown) {
return $.ajax({
type: 'PUT',
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index ccb7599e215..1d9f641836f 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -209,7 +209,7 @@
FILTER_INPUT = '.dropdown-input .dropdown-input-field';
function GitLabDropdown(el1, options) {
- var ref, ref1, ref2, ref3, searchFields, selector, self;
+ var searchFields, selector, self;
this.el = el1;
this.options = options;
this.updateLabel = bind(this.updateLabel, this);
@@ -220,7 +220,11 @@
selector = $(this.el).data("target");
this.dropdown = selector != null ? $(selector) : $(this.el).parent();
// Set Defaults
- ref = this.options, this.filterInput = (ref1 = ref.filterInput) != null ? ref1 : this.getElement(FILTER_INPUT), this.highlight = (ref2 = ref.highlight) != null ? ref2 : false, this.filterInputBlur = (ref3 = ref.filterInputBlur) != null ? ref3 : true;
+ this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
+ this.highlight = !!this.options.highlight
+ this.filterInputBlur = this.options.filterInputBlur != null
+ ? this.options.filterInputBlur
+ : true;
// If no input is passed create a default one
self = this;
// If selector was passed
@@ -419,7 +423,9 @@
var $target;
if (this.options.multiSelect) {
$target = $(e.target);
- if ($target && !$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) {
+ if ($target && !$target.hasClass('dropdown-menu-close') &&
+ !$target.hasClass('dropdown-menu-close-icon') &&
+ !$target.data('is-link')) {
e.stopPropagation();
return false;
} else {
@@ -550,6 +556,8 @@
value = this.options.id ? this.options.id(data) : data.id;
fieldName = this.options.fieldName;
+ if (value) { value = value.toString().replace(/'/g, '\\\'') };
+
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
if (field.length) {
selected = true;
@@ -621,8 +629,21 @@
selectedObject = this.renderedData[selectedIndex];
}
}
+
+ if (this.options.vue) {
+ if (el.hasClass(ACTIVE_CLASS)) {
+ el.removeClass(ACTIVE_CLASS);
+ } else {
+ el.addClass(ACTIVE_CLASS);
+ }
+
+ return selectedObject;
+ }
+
field = [];
- value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
+ value = this.options.id
+ ? this.options.id(selectedObject, el)
+ : selectedObject.id;
if (isInput) {
field = $(this.el);
} else if(value) {
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
index e111ddbe52a..dea26a3f1e1 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -30,8 +30,7 @@
ContributorsGraph.set_y_domain = function(data) {
return ContributorsGraph.prototype.y_domain = [
0, d3.max(data, function(d) {
- var ref, ref1;
- return d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions;
+ return d.commits = d.commits || d.additions || d.deletions;
})
];
};
@@ -45,8 +44,7 @@
ContributorsGraph.init_y_domain = function(data) {
return ContributorsGraph.prototype.y_domain = [
0, d3.max(data, function(d) {
- var ref, ref1;
- return d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions;
+ return d.commits = d.commits || d.additions || d.deletions;
})
];
};
@@ -148,9 +146,8 @@
return this.area = d3.svg.area().x(function(d) {
return x(d.date);
}).y0(this.height).y1(function(d) {
- var ref, ref1, xa;
- xa = d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions;
- return y(xa);
+ d.commits = d.commits || d.additions || d.deletions;
+ return y(d.commits);
}).interpolate("basis");
};
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 7449b7d47bf..849b45756ee 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -45,8 +45,8 @@
};
IssuableForm.prototype.handleSubmit = function() {
- var ref, ref1;
- if (((ref = parseInt((ref1 = this.issueMoveField) != null ? ref1.val() : void 0)) != null ? ref : 0) > 0) {
+ var fieldId = (this.issueMoveField != null) ? this.issueMoveField.val() : null;
+ if ((parseInt(fieldId) || 0) > 0) {
if (!confirm(this.issueMoveConfirmMsg)) {
return false;
}
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index b338c16ecb6..c334e3e0c02 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -23,7 +23,7 @@
abilityName = $dropdown.data('ability-name');
$selectbox = $dropdown.closest('.selectbox');
$block = $selectbox.closest('.block');
- $form = $dropdown.closest('form');
+ $form = $dropdown.closest('form, .js-issuable-update');
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
$sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
$value = $block.find('.value');
@@ -318,6 +318,7 @@
}
},
multiSelect: $dropdown.hasClass('js-multiselect'),
+ vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(label, $el, e) {
var isIssueIndex, isMRIndex, page;
_this.enableBulkLabelDropdown();
@@ -335,7 +336,7 @@
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index';
- if ($('html').hasClass('issue-boards-page')) {
+ if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
}
@@ -363,6 +364,30 @@
else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
}
+ else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ if ($el.hasClass('is-active')) {
+ gl.issueBoards.BoardsStore.detail.issue.labels.push(new ListLabel({
+ id: label.id,
+ title: label.title,
+ color: label.color[0],
+ textColor: '#fff'
+ }));
+ }
+ else {
+ var labels = gl.issueBoards.BoardsStore.detail.issue.labels;
+ labels = labels.filter(function (selectedLabel) {
+ return selectedLabel.id !== label.id;
+ });
+ gl.issueBoards.BoardsStore.detail.issue.labels = labels;
+ }
+
+ $loading.fadeIn();
+
+ gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ .then(function () {
+ $loading.fadeOut();
+ });
+ }
else {
if ($dropdown.hasClass('js-multiselect')) {
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 045f1d605f4..c909b53dc21 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -102,6 +102,7 @@
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
+ vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(selected, $el, e) {
var data, isIssueIndex, isMRIndex, page;
page = $('body').data('page');
@@ -111,7 +112,7 @@
e.preventDefault();
return;
}
- if ($('html').hasClass('issue-boards-page')) {
+ if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name;
gl.issueBoards.BoardsStore.updateFiltersUrl();
e.preventDefault();
@@ -124,6 +125,24 @@
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
+ } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ if (selected.id !== -1) {
+ Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'milestone', new ListMilestone({
+ id: selected.id,
+ title: selected.name
+ }));
+ } else {
+ Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'milestone');
+ }
+
+ $dropdown.trigger('loading.gl.dropdown');
+ $loading.fadeIn();
+
+ gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ .then(function () {
+ $dropdown.trigger('loaded.gl.dropdown');
+ $loading.fadeOut();
+ });
} else {
selected = $selectbox.find('input[type="hidden"]').val();
data = {};
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 9a8cfa554c6..2d0c6b16699 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -79,7 +79,7 @@
if (ref.header != null) {
return $('<li />').addClass('dropdown-header').text(ref.header);
} else {
- link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
+ link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', ref);
return $('<li />').append(link);
}
},
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index a5f720303dd..dcedea17a9c 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -6,15 +6,24 @@
function Sidebar(currentUser) {
this.toggleTodo = bind(this.toggleTodo, this);
this.sidebar = $('aside');
+ this.removeListeners();
this.addEventListeners();
}
+ Sidebar.prototype.removeListeners = function () {
+ this.sidebar.off('click', '.sidebar-collapsed-icon');
+ $('.dropdown').off('hidden.gl.dropdown');
+ $('.dropdown').off('loading.gl.dropdown');
+ $('.dropdown').off('loaded.gl.dropdown');
+ $(document).off('click', '.js-sidebar-toggle');
+ }
+
Sidebar.prototype.addEventListeners = function() {
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
- $(document).off('click', '.js-sidebar-toggle').on('click', '.js-sidebar-toggle', function(e, triggered) {
+ $(document).on('click', '.js-sidebar-toggle', function(e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault();
$this = $(this);
diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js
index ed5773f0a85..f9915593657 100644
--- a/app/assets/javascripts/subscription.js
+++ b/app/assets/javascripts/subscription.js
@@ -6,10 +6,10 @@
function Subscription(container) {
this.toggleSubscription = bind(this.toggleSubscription, this);
var $container;
- $container = $(container);
- this.url = $container.attr('data-url');
- this.subscribe_button = $container.find('.js-subscribe-button');
- this.subscription_status = $container.find('.subscription-status');
+ this.$container = $(container);
+ this.url = this.$container.attr('data-url');
+ this.subscribe_button = this.$container.find('.js-subscribe-button');
+ this.subscription_status = this.$container.find('.subscription-status');
this.subscribe_button.unbind('click').click(this.toggleSubscription);
}
@@ -19,17 +19,27 @@
action = btn.find('span').text();
current_status = this.subscription_status.attr('data-status');
btn.addClass('disabled');
+
+ if ($('html').hasClass('issue-boards-page')) {
+ this.url = this.$container.attr('data-url');
+ }
+
return $.post(this.url, (function(_this) {
return function() {
var status;
btn.removeClass('disabled');
- status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed';
- _this.subscription_status.attr('data-status', status);
- action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe';
- btn.find('span').text(action);
- _this.subscription_status.find('>div').toggleClass('hidden');
- if (btn.attr('data-original-title')) {
- return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle');
+
+ if ($('html').hasClass('issue-boards-page')) {
+ Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'subscribed', !gl.issueBoards.BoardsStore.detail.issue.subscribed);
+ } else {
+ status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed';
+ _this.subscription_status.attr('data-status', status);
+ action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe';
+ btn.find('span').text(action);
+ _this.subscription_status.find('>div').toggleClass('hidden');
+ if (btn.attr('data-original-title')) {
+ return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle');
+ }
}
};
})(this));
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index e198c4a1216..3847278e80a 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -10,7 +10,11 @@
this.usersPath = "/autocomplete/users.json";
this.userPath = "/autocomplete/users/:id.json";
if (currentUser != null) {
- this.currentUser = JSON.parse(currentUser);
+ if (typeof currentUser === 'object') {
+ this.currentUser = currentUser;
+ } else {
+ this.currentUser = JSON.parse(currentUser);
+ }
}
$('.js-user-search').each((function(_this) {
return function(i, dropdown) {
@@ -33,9 +37,30 @@
$value = $block.find('.value');
$collapsedSidebar = $block.find('.sidebar-collapsed-user');
$loading = $block.find('.block-loading').fadeOut();
+
+ var updateIssueBoardsIssue = function () {
+ $loading.fadeIn();
+ gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ .then(function () {
+ $loading.fadeOut();
+ });
+ };
+
$block.on('click', '.js-assign-yourself', function(e) {
e.preventDefault();
- return assignTo(_this.currentUser.id);
+
+ if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({
+ id: _this.currentUser.id,
+ username: _this.currentUser.username,
+ name: _this.currentUser.name,
+ avatar_url: _this.currentUser.avatar_url
+ }));
+
+ updateIssueBoardsIssue();
+ } else {
+ return assignTo(_this.currentUser.id);
+ }
});
assignTo = function(selected) {
var data;
@@ -151,6 +176,7 @@
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
+ vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(user, $el, e) {
var isIssueIndex, isMRIndex, page, selected;
page = $('body').data('page');
@@ -161,7 +187,7 @@
selectedId = user.id;
return;
}
- if ($('html').hasClass('issue-boards-page')) {
+ if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
selectedId = user.id;
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
gl.issueBoards.BoardsStore.updateFiltersUrl();
@@ -171,6 +197,19 @@
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
+ } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ if (user.id) {
+ Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({
+ id: user.id,
+ username: user.username,
+ name: user.name,
+ avatar_url: user.avatar_url
+ }));
+ } else {
+ Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'assignee');
+ }
+
+ updateIssueBoardsIssue();
} else {
selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val();
return assignTo(selected);
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 142076f65b2..53ee1ed309e 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -227,7 +227,7 @@ header {
float: none !important;
.visible-xs,
- .visable-sm {
+ .visible-sm {
display: table-cell !important;
}
}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index d8fabbdcebe..ef6833c9845 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -45,6 +45,15 @@
.page-with-sidebar {
padding-bottom: 0;
}
+
+ .issues-filters {
+ position: relative;
+ z-index: 999999;
+ }
+}
+
+.boards-app {
+ position: relative;
}
.boards-app-loading {
@@ -66,6 +75,10 @@
height: 475px; // Needed for PhantomJS
height: calc(100vh - 220px);
min-height: 475px;
+
+ &.is-compact {
+ width: calc(100% - 290px);
+ }
}
}
@@ -184,6 +197,10 @@
margin-bottom: 5px;
}
+ &.is-active {
+ background-color: $row-hover;
+ }
+
.label {
border: 0;
outline: 0;
@@ -212,6 +229,10 @@
margin-right: 5px;
font-size: (14px / $issue-boards-font-size) * 1em;
}
+
+ .avatar {
+ margin-left: 0;
+ }
}
.card-number {
@@ -264,3 +285,48 @@
border-width: 1px 0 1px 1px;
}
}
+
+.issue-boards-sidebar {
+ &.right-sidebar {
+ top: 153px;
+ bottom: 0;
+
+ @media (min-width: $screen-sm-min) {
+ top: 220px;
+ }
+ }
+
+ .issuable-sidebar-header {
+ position: relative;
+ }
+
+ .gutter-toggle {
+ position: absolute;
+ top: 0;
+ bottom: 15px;
+ right: 0;
+ width: 22px;
+ color: $gray-darkest;
+
+ svg {
+ position: absolute;
+ top: 50%;
+ margin-top: (-11px / 2);
+ }
+
+ &:hover {
+ path {
+ fill: $gray-darkest;
+ }
+ }
+ }
+
+ .issuable-header-text {
+ width: 100%;
+ padding-right: 35px;
+
+ > strong {
+ font-weight: 600;
+ }
+ }
+}
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index a2b01ff43dc..dc33e1405f2 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -73,10 +73,13 @@ module Projects
def serialize_as_json(resource)
resource.as_json(
labels: true,
- only: [:iid, :title, :confidential],
+ only: [:iid, :title, :confidential, :due_date],
include: {
- assignee: { only: [:id, :name, :username], methods: [:avatar_url] }
- })
+ assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
+ milestone: { only: [:id, :title] }
+ },
+ user: current_user
+ )
end
end
end
diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb
index d440edc55ba..56749d80bd3 100644
--- a/app/helpers/sidekiq_helper.rb
+++ b/app/helpers/sidekiq_helper.rb
@@ -5,7 +5,7 @@ module SidekiqHelper
(?<mem>[\d\.,]+)\s+
(?<state>[DRSTWXZNLsl\+<]+)\s+
(?<start>.+)\s+
- (?<command>sidekiq.*\])\s+
+ (?<command>sidekiq.*\])\s*
\z/x
def parse_sidekiq_ps(line)
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 89158a50353..e356fe06363 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -287,10 +287,12 @@ class Issue < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
+ json[:subscribed] = subscribed?(options[:user]) if options.has_key?(:user)
+
if options.has_key?(:labels)
json[:labels] = labels.as_json(
project: project,
- only: [:id, :title, :description, :color],
+ only: [:id, :title, :description, :color, :priority],
methods: [:text_color]
)
end
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index d16bd61b779..070ed90da6d 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -461,7 +461,7 @@
.panel-body
= lorem
- %h2#alert Alerts
+ %h2#alerts Alerts
.row
.col-md-6
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
index c6d718a1cd1..8fce702314c 100644
--- a/app/views/projects/boards/components/_card.html.haml
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -7,8 +7,11 @@
":issue-link-base" => "issueLinkBase",
":disabled" => "disabled",
"track-by" => "id" }
- %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id }",
- ":index" => "index" }
+ %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }",
+ ":index" => "index",
+ "@mousedown" => "mouseDown",
+ "@mouseMove" => "mouseMove",
+ "@mouseup" => "showIssue($event)" }
%h4.card-title
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
%a{ ":href" => "issueLinkBase + '/' + issue.id",
@@ -18,6 +21,11 @@
%span.card-number{ "v-if" => "issue.id" }
= precede '#' do
{{ issue.id }}
+ %a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username",
+ ":title" => "'Assigned to ' + issue.assignee.name",
+ "v-if" => "issue.assignee",
+ data: { container: 'body' } }
+ %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
%button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
type: "button",
"v-if" => "(!list.label || label.id !== list.label.id)",
@@ -26,8 +34,3 @@
":title" => "label.description",
data: { container: 'body' } }
{{ label.title }}
- %a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username",
- ":title" => "'Assigned to ' + issue.assignee.name",
- "v-if" => "issue.assignee",
- data: { container: 'body' } }
- %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml
new file mode 100644
index 00000000000..f0c0c6953e0
--- /dev/null
+++ b/app/views/projects/boards/components/_sidebar.html.haml
@@ -0,0 +1,23 @@
+%board-sidebar{ "inline-template" => true,
+ ":current-user" => "#{current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) if current_user}" }
+ %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
+ .issuable-sidebar
+ .block.issuable-sidebar-header
+ %span.issuable-header-text.hide-collapsed.pull-left
+ %strong
+ {{ issue.title }}
+ %br/
+ %span
+ = precede "#" do
+ {{ issue.id }}
+ %a.gutter-toggle.pull-right{ role: "button",
+ href: "#",
+ "@click.prevent" => "closeSidebar",
+ "aria-label" => "Toggle sidebar" }
+ = custom_icon("icon_close", size: 15)
+ .js-issuable-update
+ = render "projects/boards/components/sidebar/assignee"
+ = render "projects/boards/components/sidebar/milestone"
+ = render "projects/boards/components/sidebar/due_date"
+ = render "projects/boards/components/sidebar/labels"
+ = render "projects/boards/components/sidebar/notifications"
diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml
new file mode 100644
index 00000000000..604e13858d1
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_assignee.html.haml
@@ -0,0 +1,40 @@
+.block.assignee
+ .title.hide-collapsed
+ Assignee
+ = icon("spinner spin", class: "block-loading")
+ - if can?(current_user, :admin_issue, @project)
+ = link_to "Edit", "#", class: "edit-link pull-right"
+ .value.hide-collapsed
+ %span.assign-yourself.no-value{ "v-if" => "!issue.assignee" }
+ No assignee
+ - if can?(current_user, :admin_issue, @project)
+ \-
+ %a.js-assign-yourself{ href: "#" }
+ assign yourself
+ %a.author_link.bold{ ":href" => "'#{root_url}' + issue.assignee.username",
+ "v-if" => "issue.assignee" }
+ %img.avatar.avatar-inline.s32{ ":src" => "issue.assignee.avatar",
+ width: "32" }
+ %span.author
+ {{ issue.assignee.name }}
+ %span.username
+ = precede "@" do
+ {{ issue.assignee.username }}
+ - if can?(current_user, :admin_issue, @project)
+ .selectbox.hide-collapsed
+ %input{ type: "hidden",
+ name: "issue[assignee_id]",
+ id: "issue_assignee_id",
+ ":value" => "issue.assignee.id",
+ "v-if" => "issue.assignee" }
+ .dropdown
+ %button.dropdown-menu-toggle.js-user-search.js-author-search.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", field_name: "issue[assignee_id]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true" },
+ ":data-issuable-id" => "issue.id",
+ ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
+ Select assignee
+ = icon("chevron-down")
+ .dropdown-menu.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
+ = dropdown_title("Assign to")
+ = dropdown_filter("Search users")
+ = dropdown_content
+ = dropdown_loading
diff --git a/app/views/projects/boards/components/sidebar/_due_date.html.haml b/app/views/projects/boards/components/sidebar/_due_date.html.haml
new file mode 100644
index 00000000000..c7da1d0d4ac
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_due_date.html.haml
@@ -0,0 +1,32 @@
+.block.due_date
+ .title
+ Due date
+ = icon("spinner spin", class: "block-loading")
+ - if can?(current_user, :admin_issue, @project)
+ = link_to "Edit", "#", class: "edit-link pull-right"
+ .value
+ .value-content
+ %span.no-value{ "v-if" => "!issue.dueDate" }
+ No due date
+ %span.bold{ "v-if" => "issue.dueDate" }
+ {{ issue.dueDate | due-date }}
+ - if can?(current_user, :admin_issue, @project)
+ %span.no-value.js-remove-due-date-holder{ "v-if" => "issue.dueDate" }
+ \-
+ %a.js-remove-due-date{ href: "#", role: "button" }
+ remove due date
+ - if can?(current_user, :admin_issue, @project)
+ .selectbox
+ %input{ type: "hidden",
+ name: "issue[due_date]",
+ ":value" => "issue.dueDate" }
+ .dropdown
+ %button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button',
+ data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" },
+ ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
+ %span.dropdown-toggle-text Due date
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-menu-due-date
+ = dropdown_title('Due date')
+ = dropdown_content do
+ .js-due-date-calendar
diff --git a/app/views/projects/boards/components/sidebar/_labels.html.haml b/app/views/projects/boards/components/sidebar/_labels.html.haml
new file mode 100644
index 00000000000..ce68e5e1998
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_labels.html.haml
@@ -0,0 +1,30 @@
+.block.labels
+ .title
+ Labels
+ = icon("spinner spin", class: "block-loading")
+ - if can?(current_user, :admin_issue, @project)
+ = link_to "Edit", "#", class: "edit-link pull-right"
+ .value.issuable-show-labels
+ %span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
+ None
+ %a{ href: "#",
+ "v-for" => "label in issue.labels" }
+ %span.label.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
+ {{ label.title }}
+ - if can?(current_user, :admin_issue, @project)
+ .selectbox
+ %input{ type: "hidden",
+ name: "issue[label_names][]",
+ "v-for" => "label in issue.labels",
+ ":value" => "label.id" }
+ .dropdown
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button",
+ data: { toggle: "dropdown", field_name: "issue[label_names][]", show_no: "true", show_any: "true", project_id: @project.id, labels: namespace_project_labels_path(@project.namespace, @project, :json), namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) },
+ ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
+ %span.dropdown-toggle-text
+ Label
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ = render partial: "shared/issuable/label_page_default"
+ - if can? current_user, :admin_label, @project and @project
+ = render partial: "shared/issuable/label_page_create"
diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/projects/boards/components/sidebar/_milestone.html.haml
new file mode 100644
index 00000000000..3cd20d1c0f7
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_milestone.html.haml
@@ -0,0 +1,28 @@
+.block.milestone
+ .title
+ Milestone
+ = icon("spinner spin", class: "block-loading")
+ - if can?(current_user, :admin_issue, @project)
+ = link_to "Edit", "#", class: "edit-link pull-right"
+ .value
+ %span.no-value{ "v-if" => "!issue.milestone" }
+ None
+ %span.bold.has-tooltip{ "v-if" => "issue.milestone" }
+ {{ issue.milestone.title }}
+ - if can?(current_user, :admin_issue, @project)
+ .selectbox
+ %input{ type: "hidden",
+ ":value" => "issue.milestone.id",
+ name: "issue[milestone_id]",
+ "v-if" => "issue.milestone" }
+ .dropdown
+ %button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: "issue", use_id: "true" },
+ ":data-issuable-id" => "issue.id",
+ ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
+ Milestone
+ = icon("chevron-down")
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ = dropdown_title("Assignee milestone")
+ = dropdown_filter("Search milestones")
+ = dropdown_content
+ = dropdown_loading
diff --git a/app/views/projects/boards/components/sidebar/_notifications.html.haml b/app/views/projects/boards/components/sidebar/_notifications.html.haml
new file mode 100644
index 00000000000..21c9563e9db
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_notifications.html.haml
@@ -0,0 +1,11 @@
+- if current_user
+ .block.light.subscription{ ":data-url" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '/toggle_subscription'" }
+ .title
+ Notifications
+ %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
+ {{ issue.subscribed ? 'Unsubscribe' : 'Subscribe' }}
+ .subscription-status{ ":data-status" => "issue.subscribed ? 'subscribed' : 'unsubscribed'" }
+ .unsubscribed{ "v-show" => "!issue.subscribed" }
+ You're not receiving notifications from this thread.
+ .subscribed{ "v-show" => "issue.subscribed" }
+ You're receiving notifications because you're subscribed to this thread.
diff --git a/app/views/projects/boards/index.html.haml b/app/views/projects/boards/index.html.haml
index 885f8e34b55..29c9a43a0c1 100644
--- a/app/views/projects/boards/index.html.haml
+++ b/app/views/projects/boards/index.html.haml
@@ -10,7 +10,9 @@
= render 'shared/issuable/filter', type: :boards
-.boards-list#board-app{ "v-cloak" => true, data: board_data }
- .boards-app-loading.text-center{ "v-if" => "loading" }
- = icon("spinner spin")
- = render "projects/boards/components/board"
+#board-app.boards-app{ "v-cloak" => true, data: board_data }
+ .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
+ .boards-app-loading.text-center{ "v-if" => "loading" }
+ = icon("spinner spin")
+ = render "projects/boards/components/board"
+ = render "projects/boards/components/sidebar"
diff --git a/app/views/projects/boards/show.html.haml b/app/views/projects/boards/show.html.haml
index 885f8e34b55..29c9a43a0c1 100644
--- a/app/views/projects/boards/show.html.haml
+++ b/app/views/projects/boards/show.html.haml
@@ -10,7 +10,9 @@
= render 'shared/issuable/filter', type: :boards
-.boards-list#board-app{ "v-cloak" => true, data: board_data }
- .boards-app-loading.text-center{ "v-if" => "loading" }
- = icon("spinner spin")
- = render "projects/boards/components/board"
+#board-app.boards-app{ "v-cloak" => true, data: board_data }
+ .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
+ .boards-app-loading.text-center{ "v-if" => "loading" }
+ = icon("spinner spin")
+ = render "projects/boards/components/board"
+ = render "projects/boards/components/sidebar"
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 28aad3f4725..78aa9fb7391 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -1,5 +1,5 @@
/ Side-by-side diff view
-%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight{ data: diff_view_data }
+%div.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data }
%table
- last_line = 0
- diff_file.parallel_diff_lines.each do |line|
diff --git a/app/views/shared/icons/_icon_close.svg b/app/views/shared/icons/_icon_close.svg
new file mode 100644
index 00000000000..9d62012518b
--- /dev/null
+++ b/app/views/shared/icons/_icon_close.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="M9,7.5l5.83-5.91a.48.48,0,0,0,0-.69L14.11.15a.46.46,0,0,0-.68,0l-5.93,6L1.57.15a.46.46,0,0,0-.68,0L.15.9a.48.48,0,0,0,0,.69L6,7.5.15,13.41a.48.48,0,0,0,0,.69l.74.75a.46.46,0,0,0,.68,0l5.93-6,5.93,6a.46.46,0,0,0,.68,0l.74-.75a.48.48,0,0,0,0-.69Z"/></svg> \ No newline at end of file
diff --git a/doc/api/services.md b/doc/api/services.md
index 579fdc0c8c9..c7f537aceb6 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -451,43 +451,49 @@ GET /projects/:id/services/irker
## JIRA
-Jira issue tracker
+JIRA issue tracker.
+
+### Get JIRA service settings
+
+Get JIRA service settings for a project.
+
+```
+GET /projects/:id/services/jira
+```
### Create/Edit JIRA service
Set JIRA service for a project.
-> Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to easily navigate to the Jira issue tracker. See the [integration doc](http://docs.gitlab.com/ce/integration/external-issue-tracker.html) for details. Support for referencing commits and automatic closing of Jira issues directly from GitLab is [available in GitLab EE.](http://docs.gitlab.com/ee/integration/jira.html)
+>**Note:**
+Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to
+easily navigate to the JIRA issue tracker. See the [integration doc][jira-doc]
+for details.
```
PUT /projects/:id/services/jira
```
-Parameters:
-
-- `new_issue_url` (**required**) - New Issue url
-- `project_url` (**required**) - Project url
-- `issues_url` (**required**) - Issue url
-- `description` (optional) - Jira issue tracker
-- `username` (optional) - Jira username
-- `password` (optional) - Jira password
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `active` | boolean| no | Enable/disable the JIRA service. |
+| `project_url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project. It is of the form: `https://<jira_host_url>/issues/?jql=project=<jira_project>`. |
+| `issues_url` | string | yes | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. It is of the form: `https://<jira_host_url>/browse/:id`. Leave `:id` as-is, it gets replaced by GitLab at runtime.|
+| `new_issue_url` | string | yes | This is the URL to create a new issue in JIRA for the project linked to this GitLab project, and it is of the form: `https://<jira_host_url>/secure/CreateIssue.jspa` |
+| `api_url` | string | yes | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https://<jira_host_url>/rest/api/2`. |
+| `description` | string | no | A name for the issue tracker. |
+| `username` | string | no | The username of the user created to be used with GitLab/JIRA. |
+| `password` | string | no | The password of the user created to be used with GitLab/JIRA. |
+| `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. |
### Delete JIRA service
-Delete JIRA service for a project.
+Remove all previously JIRA settings from a project.
```
DELETE /projects/:id/services/jira
```
-### Get JIRA service settings
-
-Get JIRA service settings for a project.
-
-```
-GET /projects/:id/services/jira
-```
-
## PivotalTracker
Project Management Software (Source Commits Endpoint)
@@ -662,3 +668,5 @@ Get JetBrains TeamCity CI service settings for a project.
```
GET /projects/:id/services/teamcity
```
+
+[jira-doc]: ../project_services/jira.md
diff --git a/doc/install/installation.md b/doc/install/installation.md
index c9acc9cdfb0..795e1d23443 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -142,6 +142,9 @@ gitlab-workhorse we need a Go compiler. The instructions below assume you
use 64-bit Linux. You can find downloads for other platforms at the [Go download
page](https://golang.org/dl).
+ # Remove former Go installation folder
+ sudo rm -rf /usr/local/go
+
curl --remote-name --progress https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz
echo '43afe0c5017e502630b1aea4d44b8a7f059bf60d7f29dfd58db454d4e4e0ae53 go1.5.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
sudo tar -C /usr/local -xzf go1.5.3.linux-amd64.tar.gz
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 825e05fbae3..425df2c176a 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -49,18 +49,23 @@ module API
attrs = attributes_for_keys [:title, :key]
attrs[:key].strip! if attrs[:key]
+ # Check for an existing key joined to this project
key = user_project.deploy_keys.find_by(key: attrs[:key])
- present key, with: Entities::SSHKey if key
+ if key
+ present key, with: Entities::SSHKey
+ break
+ end
# Check for available deploy keys in other projects
key = current_user.accessible_deploy_keys.find_by(key: attrs[:key])
if key
user_project.deploy_keys << key
present key, with: Entities::SSHKey
+ break
end
+ # Create a new deploy key
key = DeployKey.new attrs
-
if key.valid? && user_project.deploy_keys << key
present key, with: Entities::SSHKey
else
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 7b675e05fbb..bf2a199ce21 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -4,25 +4,24 @@ module API
before { authenticate! }
before { authorize! :download_code, user_project }
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
- # Get a project repository tags
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # GET /projects/:id/repository/tags
+ desc 'Get a project repository tags' do
+ success Entities::RepoTag
+ end
get ":id/repository/tags" do
present user_project.repository.tags.sort_by(&:name).reverse,
with: Entities::RepoTag, project: user_project
end
- # Get a single repository tag
- #
- # Parameters:
- # id (required) - The ID of a project
- # tag_name (required) - The name of the tag
- # Example Request:
- # GET /projects/:id/repository/tags/:tag_name
+ desc 'Get a single repository tag' do
+ success Entities::RepoTag
+ end
+ params do
+ requires :tag_name, type: String, desc: 'The name of the tag'
+ end
get ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do
tag = user_project.repository.find_tag(params[:tag_name])
not_found!('Tag') unless tag
@@ -30,20 +29,21 @@ module API
present tag, with: Entities::RepoTag, project: user_project
end
- # Create tag
- #
- # Parameters:
- # id (required) - The ID of a project
- # tag_name (required) - The name of the tag
- # ref (required) - Create tag from commit sha or branch
- # message (optional) - Specifying a message creates an annotated tag.
- # Example Request:
- # POST /projects/:id/repository/tags
+ desc 'Create a new repository tag' do
+ success Entities::RepoTag
+ end
+ params do
+ requires :tag_name, type: String, desc: 'The name of the tag'
+ requires :ref, type: String, desc: 'The commit sha or branch name'
+ optional :message, type: String, desc: 'Specifying a message creates an annotated tag'
+ optional :release_description, type: String, desc: 'Specifying release notes stored in the GitLab database'
+ end
post ':id/repository/tags' do
authorize_push_project
- message = params[:message] || nil
+ create_params = declared(params)
+
result = CreateTagService.new(user_project, current_user).
- execute(params[:tag_name], params[:ref], message, params[:release_description])
+ execute(create_params[:tag_name], create_params[:ref], create_params[:message], create_params[:release_description])
if result[:status] == :success
present result[:tag],
@@ -54,15 +54,13 @@ module API
end
end
- # Delete tag
- #
- # Parameters:
- # id (required) - The ID of a project
- # tag_name (required) - The name of the tag
- # Example Request:
- # DELETE /projects/:id/repository/tags/:tag
+ desc 'Delete a repository tag'
+ params do
+ requires :tag_name, type: String, desc: 'The name of the tag'
+ end
delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do
authorize_push_project
+
result = DeleteTagService.new(user_project, current_user).
execute(params[:tag_name])
@@ -75,17 +73,16 @@ module API
end
end
- # Add release notes to tag
- #
- # Parameters:
- # id (required) - The ID of a project
- # tag_name (required) - The name of the tag
- # description (required) - Release notes with markdown support
- # Example Request:
- # POST /projects/:id/repository/tags/:tag_name/release
+ desc 'Add a release note to a tag' do
+ success Entities::Release
+ end
+ params do
+ requires :tag_name, type: String, desc: 'The name of the tag'
+ requires :description, type: String, desc: 'Release notes with markdown support'
+ end
post ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do
authorize_push_project
- required_attributes! [:description]
+
result = CreateReleaseService.new(user_project, current_user).
execute(params[:tag_name], params[:description])
@@ -96,17 +93,16 @@ module API
end
end
- # Updates a release notes of a tag
- #
- # Parameters:
- # id (required) - The ID of a project
- # tag_name (required) - The name of the tag
- # description (required) - Release notes with markdown support
- # Example Request:
- # PUT /projects/:id/repository/tags/:tag_name/release
+ desc "Update a tag's release note" do
+ success Entities::Release
+ end
+ params do
+ requires :tag_name, type: String, desc: 'The name of the tag'
+ requires :description, type: String, desc: 'Release notes with markdown support'
+ end
put ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do
authorize_push_project
- required_attributes! [:description]
+
result = UpdateReleaseService.new(user_project, current_user).
execute(params[:tag_name], params[:description])
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
index da59642f24d..cbe0417a4a7 100644
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -21,9 +21,11 @@ describe Projects::Boards::IssuesController do
context 'with valid list id' do
it 'returns issues that have the list label applied' do
johndoe = create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png')))
+ issue = create(:labeled_issue, project: project, labels: [planning])
create(:labeled_issue, project: project, labels: [planning])
- create(:labeled_issue, project: project, labels: [development])
+ create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
+ issue.subscribe(johndoe)
list_issues user: user, board: board, list: list2
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index 67d6da5f39a..760a8967123 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -66,6 +66,21 @@ describe 'Issue Boards new issue', feature: true, js: true do
expect(page).to have_content('1')
end
end
+
+ it 'shows sidebar when creating new issue' do
+ page.within(first('.board')) do
+ find('.board-issue-count-holder .btn').click
+ end
+
+ page.within(first('.board-new-issue-form')) do
+ find('.form-control').set('bug')
+ click_button 'Submit issue'
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.issue-boards-sidebar')
+ end
end
context 'unauthorized user' do
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
new file mode 100644
index 00000000000..f160052a844
--- /dev/null
+++ b/spec/features/boards/sidebar_spec.rb
@@ -0,0 +1,312 @@
+require 'rails_helper'
+
+describe 'Issue Boards', feature: true, js: true do
+ include WaitForAjax
+ include WaitForVueResource
+
+ let(:project) { create(:empty_project, :public) }
+ let(:board) { create(:board, project: project) }
+ let(:user) { create(:user) }
+ let!(:label) { create(:label, project: project) }
+ let!(:label2) { create(:label, project: project) }
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:issue2) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [label]) }
+ let!(:issue) { create(:issue, project: project) }
+
+ before do
+ project.team << [user, :master]
+
+ login_as(user)
+
+ visit namespace_project_board_path(project.namespace, project, board)
+ wait_for_vue_resource
+ end
+
+ it 'shows sidebar when clicking issue' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ expect(page).to have_selector('.issue-boards-sidebar')
+ end
+
+ it 'closes sidebar when clicking issue' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ expect(page).to have_selector('.issue-boards-sidebar')
+
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ expect(page).not_to have_selector('.issue-boards-sidebar')
+ end
+
+ it 'closes sidebar when clicking close button' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ expect(page).to have_selector('.issue-boards-sidebar')
+
+ find('.gutter-toggle').click
+
+ expect(page).not_to have_selector('.issue-boards-sidebar')
+ end
+
+ it 'shows issue details when sidebar is open' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.issue-boards-sidebar') do
+ expect(page).to have_content(issue.title)
+ expect(page).to have_content(issue.to_reference)
+ end
+ end
+
+ context 'assignee' do
+ it 'updates the issues assignee' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.assignee') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ page.within('.dropdown-menu-user') do
+ click_link user.name
+
+ wait_for_vue_resource
+ end
+
+ expect(page).to have_content(user.name)
+ end
+
+ page.within(first('.board')) do
+ page.within(first('.card')) do
+ expect(page).to have_selector('.avatar')
+ end
+ end
+ end
+
+ it 'removes the assignee' do
+ page.within(first('.board')) do
+ find('.card:nth-child(2)').click
+ end
+
+ page.within('.assignee') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ page.within('.dropdown-menu-user') do
+ click_link 'Unassigned'
+
+ wait_for_vue_resource
+ end
+
+ expect(page).to have_content('No assignee')
+ end
+
+ page.within(first('.board')) do
+ page.within(find('.card:nth-child(2)')) do
+ expect(page).not_to have_selector('.avatar')
+ end
+ end
+ end
+
+ it 'assignees to current user' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.assignee') do
+ click_link 'assign yourself'
+
+ wait_for_vue_resource
+
+ expect(page).to have_content(user.name)
+ end
+
+ page.within(first('.board')) do
+ page.within(first('.card')) do
+ expect(page).to have_selector('.avatar')
+ end
+ end
+ end
+ end
+
+ context 'milestone' do
+ it 'adds a milestone' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.milestone') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ click_link milestone.title
+
+ wait_for_vue_resource
+
+ page.within('.value') do
+ expect(page).to have_content(milestone.title)
+ end
+ end
+ end
+
+ it 'removes a milestone' do
+ page.within(first('.board')) do
+ find('.card:nth-child(2)').click
+ end
+
+ page.within('.milestone') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ click_link "No Milestone"
+
+ wait_for_vue_resource
+
+ page.within('.value') do
+ expect(page).not_to have_content(milestone.title)
+ end
+ end
+ end
+ end
+
+ context 'due date' do
+ it 'updates due date' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.due_date') do
+ click_link 'Edit'
+
+ click_link Date.today.day
+
+ wait_for_vue_resource
+
+ expect(page).to have_content(Date.today.to_s(:medium))
+ end
+ end
+ end
+
+ context 'labels' do
+ it 'adds a single label' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.labels') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ click_link label.title
+
+ wait_for_vue_resource
+
+ find('.dropdown-menu-close-icon').click
+
+ page.within('.value') do
+ expect(page).to have_selector('.label', count: 1)
+ expect(page).to have_content(label.title)
+ end
+ end
+
+ page.within(first('.board')) do
+ page.within(first('.card')) do
+ expect(page).to have_selector('.label', count: 1)
+ expect(page).to have_content(label.title)
+ end
+ end
+ end
+
+ it 'adds a multiple labels' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.labels') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ click_link label.title
+ click_link label2.title
+
+ wait_for_vue_resource
+
+ find('.dropdown-menu-close-icon').click
+
+ page.within('.value') do
+ expect(page).to have_selector('.label', count: 2)
+ expect(page).to have_content(label.title)
+ expect(page).to have_content(label2.title)
+ end
+ end
+
+ page.within(first('.board')) do
+ page.within(first('.card')) do
+ expect(page).to have_selector('.label', count: 2)
+ expect(page).to have_content(label.title)
+ expect(page).to have_content(label2.title)
+ end
+ end
+ end
+
+ it 'removes a label' do
+ page.within(first('.board')) do
+ find('.card:nth-child(2)').click
+ end
+
+ page.within('.labels') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ click_link label.title
+
+ wait_for_vue_resource
+
+ find('.dropdown-menu-close-icon').click
+
+ page.within('.value') do
+ expect(page).to have_selector('.label', count: 0)
+ expect(page).not_to have_content(label.title)
+ end
+ end
+
+ page.within(first('.board')) do
+ page.within(find('.card:nth-child(2)')) do
+ expect(page).not_to have_selector('.label', count: 1)
+ expect(page).not_to have_content(label.title)
+ end
+ end
+ end
+ end
+
+ context 'subscription' do
+ it 'changes issue subscription' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.subscription') do
+ click_button 'Subscribe'
+
+ expect(page).to have_content("You're receiving notifications because you're subscribed to this thread.")
+ end
+ end
+ end
+end
diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb
index 485dc560061..88e1549a22b 100644
--- a/spec/features/issues/filter_by_milestone_spec.rb
+++ b/spec/features/issues/filter_by_milestone_spec.rb
@@ -58,6 +58,22 @@ feature 'Issue filtering by Milestone', feature: true do
expect(page).to have_css('.issue', count: 1)
end
+ context 'when milestone has single quotes in title' do
+ background do
+ milestone.update(name: "rock 'n' roll")
+ end
+
+ scenario 'filters by a specific Milestone', js: true do
+ create(:issue, project: project, milestone: milestone)
+ create(:issue, project: project)
+
+ visit_issues(project)
+ filter_by_milestone(milestone.title)
+
+ expect(page).to have_css('.issue', count: 1)
+ end
+ end
+
def visit_issues(project)
visit namespace_project_issues_path(project.namespace, project)
end
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index d917d5950ec..f6e9230c8da 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -67,6 +67,23 @@ feature 'Merge Request filtering by Milestone', feature: true do
expect(page).to have_css('.merge-request', count: 1)
end
+ context 'when milestone has single quotes in title' do
+ background do
+ milestone.update(name: "rock 'n' roll")
+ end
+
+ scenario 'filters by a specific Milestone', js: true do
+ create(:merge_request, :with_diffs, source_project: project, milestone: milestone)
+ create(:merge_request, :simple, source_project: project)
+
+ visit_merge_requests(project)
+ filter_by_milestone(milestone.title)
+
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
+ expect(page).to have_css('.merge-request', count: 1)
+ end
+ end
+
def visit_merge_requests(project)
visit namespace_project_merge_requests_path(project.namespace, project)
end
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index 532ebb9640e..77f2bcee1f3 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -9,6 +9,7 @@
"iid": { "type": "integer" },
"title": { "type": "string" },
"confidential": { "type": "boolean" },
+ "due_date": { "type": ["date", "null"] },
"labels": {
"type": "array",
"items": {
@@ -42,7 +43,8 @@
"name": { "type": "string" },
"username": { "type": "string" },
"avatar_url": { "type": "uri" }
- }
+ },
+ "subscribed": { "type": ["boolean", "null"] }
},
"additionalProperties": false
}
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index 7d8cc45327c..65897edba7f 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -6,6 +6,7 @@ describe API::API, api: true do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:project, creator_id: user.id) }
+ let(:project2) { create(:project, creator_id: user.id) }
let(:deploy_key) { create(:deploy_key, public: true) }
let!(:deploy_keys_project) do
@@ -96,6 +97,22 @@ describe API::API, api: true do
post api("/projects/#{project.id}/deploy_keys", admin), key_attrs
end.to change{ project.deploy_keys.count }.by(1)
end
+
+ it 'returns an existing ssh key when attempting to add a duplicate' do
+ expect do
+ post api("/projects/#{project.id}/deploy_keys", admin), { key: deploy_key.key, title: deploy_key.title }
+ end.not_to change { project.deploy_keys.count }
+
+ expect(response).to have_http_status(201)
+ end
+
+ it 'joins an existing ssh key to a new project' do
+ expect do
+ post api("/projects/#{project2.id}/deploy_keys", admin), { key: deploy_key.key, title: deploy_key.title }
+ end.to change { project2.deploy_keys.count }.by(1)
+
+ expect(response).to have_http_status(201)
+ end
end
describe 'DELETE /projects/:id/deploy_keys/:key_id' do