Commit 7cdb238a authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into show-status-from-branch

* upstream/master: (65 commits)
  Fixed typo in css class
  Merge branch 'airat/gitlab-ce-23268-fix-milestones-filtering' into 'master'
  Escape quotes in gl_dropdown values to prevent exceptions
  Fixes various errors when adding deploy keys caused by not exiting the control flow.
  Fix typo on /help/ui to Alerts section
  Grapify tags API
  Add 8.13.1 CHANGELOG entries
  Fix sidekiq stats in admin area
  Remove use of wait_for_ajax since jQuery was removed
  Specify which Fog storage drivers are imported by default in backup_restore.md
  Moved avatar infront of labels
  Don't schedule ProjectCacheWorker unless needed
  Fixed height of sidebar causing scrolling issues
  Reduce overhead of LabelFinder by avoiding #presence call
  Fixed users profile link in sidebar Fixed new labels not being created
  Improve redis config tasks for migration paths job
  Ensure search val is defined.
  Ensure cursor is applied to end of issues search input.
  Increase debounce wait on issues search execution.
  Keep the new resque.yml aside and use it once we've checked out master
  ...
parents 51a012b8 0e544db6
...@@ -279,16 +279,20 @@ bundler:audit: ...@@ -279,16 +279,20 @@ bundler:audit:
migration paths: migration paths:
stage: test stage: test
<<: *use-db <<: *use-db
variables:
SETUP_DB: "false"
only: only:
- master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ce
script: script:
- git checkout HEAD . - git checkout HEAD .
- git fetch --tags - git fetch --tags
- git checkout v8.5.9 - git checkout v8.5.9
- 'echo test: unix:/var/opt/gitlab/redis/redis.socket > config/resque.yml' - cp config/resque.yml.example config/resque.yml
- sed -i 's/localhost/redis/g' config/resque.yml
- bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" --retry=3 - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" --retry=3
- rake db:drop db:create db:schema:load db:seed_fu - rake db:drop db:create db:schema:load db:seed_fu
- git checkout $CI_BUILD_REF - git checkout $CI_BUILD_REF
- source scripts/prepare_build.sh
- rake db:migrate - rake db:migrate
coverage: coverage:
......
...@@ -8,29 +8,45 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -8,29 +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 extra space on Build sidebar on Firefox !7060
- Fix HipChat notifications rendering (airatshigapov, eisnerd) - Fix HipChat notifications rendering (airatshigapov, eisnerd)
- Add hover to trash icon in notes !7008 (blackst0ne) - 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) - 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)
- Simpler arguments passed to named_route on toggle_award_url helper method - 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 - Fix: Backup restore doesn't clear cache
- Fix showing pipeline status for a given commit from correct branch !7034 - Fix showing pipeline status for a given commit from correct branch !7034
- 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 - 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 Sign in page 'Forgot your password?' link overlaps on medium-large screens
- Fix documents and comments on Build API `scope` - Fix documents and comments on Build API `scope`
- Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov) - Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov)
## 8.13.1 (unreleased) ## 8.13.1 (2016-10-25)
- Fix bug where labels would be assigned to issues that were moved - Fix branch protection API. !6215
- Fix error in generating labels - Fix hidden pipeline graph on commit and MR page. !6895
- Fix reply-by-email not working due to queue name mismatch - Fix Cycle analytics not showing correct data when filtering by date. !6906
- Fixed hidden pipeline graph on commit and MR page !6895 - Ensure custom provider tab labels don't break layout. !6993
- Expire and build repository cache after project import - Fix issue boards user link when in subdirectory. !7018
- Fix 404 for group pages when GitLab setup uses relative url - Refactor and add new environment functionality to CI yaml reference. !7026
- Simpler arguments passed to named_route on toggle_award_url helper method - Fix typo in project settings that prevents users from enabling container registry. !7037
- Fix unauthorized users dragging on issue boards - Fix events order in `users/:id/events` endpoint. !7039
- Better handle when no users were selected for adding to group or project. (Linus Thiel) - Remove extra line for empty issue description. !7045
- Only show register tab if signup enabled. - 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) ## 8.13.0 (2016-10-22)
- Removes extra line for empty issue description. (!7045)
- Fix save button on project pipeline settings page. (!6955) - Fix save button on project pipeline settings page. (!6955)
- All Sidekiq workers now use their own queue - All Sidekiq workers now use their own queue
- Avoid race condition when asynchronously removing expired artifacts. (!6881) - Avoid race condition when asynchronously removing expired artifacts. (!6881)
...@@ -51,7 +67,6 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -51,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 - Update duration at the end of pipeline
- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
- Add group level labels. (!6425) - 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) - Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun)
- Cancelled pipelines could be retried. !6927 - Cancelled pipelines could be retried. !6927
- Updating verbiage on git basics to be more intuitive - Updating verbiage on git basics to be more intuitive
...@@ -59,7 +74,6 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -59,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) - Clarify documentation for Runners API (Gennady Trafimenkov)
- The instrumentation for Banzai::Renderer has been restored - The instrumentation for Banzai::Renderer has been restored
- Change user & group landing page routing from /u/:username to /:username - Change user & group landing page routing from /u/:username to /:username
- Fixed issue boards user link when in subdirectory
- Added documentation for .gitattributes files - Added documentation for .gitattributes files
- Move Pipeline Metrics to separate worker - Move Pipeline Metrics to separate worker
- AbstractReferenceFilter caches project_refs on RequestStore when active - AbstractReferenceFilter caches project_refs on RequestStore when active
...@@ -76,6 +90,7 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -76,6 +90,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs) - Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
- Add tag shortcut from the Commit page. !6543 - Add tag shortcut from the Commit page. !6543
- Keep refs for each deployment - Keep refs for each deployment
- Close open tooltips on page navigation (Linus Thiel)
- Allow browsing branches that end with '.atom' - Allow browsing branches that end with '.atom'
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller) - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
- Replace unique keyframes mixin with keyframe mixin with specific names (ClemMakesApps) - Replace unique keyframes mixin with keyframe mixin with specific names (ClemMakesApps)
...@@ -103,6 +118,7 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -103,6 +118,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Add RTL support to markdown renderer (Ebrahim Byagowi) - Add RTL support to markdown renderer (Ebrahim Byagowi)
- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps) - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
- Fix todos page mobile viewport layout (ClemMakesApps) - Fix todos page mobile viewport layout (ClemMakesApps)
- Make issues search less finicky
- Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps) - Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps)
- Remove redundant mixins (ClemMakesApps) - Remove redundant mixins (ClemMakesApps)
- Added 'Download' button to the Snippets page (Justin DiPierro) - Added 'Download' button to the Snippets page (Justin DiPierro)
...@@ -404,7 +420,6 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -404,7 +420,6 @@ Please view this file on the master branch, on stable branches it's out of date.
- Fix inconsistent checkbox alignment (ClemMakesApps) - Fix inconsistent checkbox alignment (ClemMakesApps)
- Use the default branch for displaying the project icon instead of master !5792 (Hannes Rosenögger) - 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 - 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 - Fix hover leading space bug in pipeline graph !5980
- Avoid conflict with admin labels when importing GitHub labels - Avoid conflict with admin labels when importing GitHub labels
- User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496 - User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496
......
...@@ -24,9 +24,7 @@ ...@@ -24,9 +24,7 @@
var filter = sender.attr("id").split("_")[0]; var filter = sender.attr("id").split("_")[0];
$('.event-filter .active').removeClass("active"); $('.event-filter .active').removeClass("active");
$.cookie("event_filter", filter, { Cookies.set("event_filter", filter);
path: gon.relative_url_root || '/'
});
sender.closest('li').toggleClass("active"); sender.closest('li').toggleClass("active");
}; };
......
...@@ -11,13 +11,13 @@ ...@@ -11,13 +11,13 @@
/*= require jquery-ui/effect-highlight */ /*= require jquery-ui/effect-highlight */
/*= require jquery-ui/sortable */ /*= require jquery-ui/sortable */
/*= require jquery_ujs */ /*= require jquery_ujs */
/*= require jquery.cookie */
/*= require jquery.endless-scroll */ /*= require jquery.endless-scroll */
/*= require jquery.highlight */ /*= require jquery.highlight */
/*= require jquery.waitforimages */ /*= require jquery.waitforimages */
/*= require jquery.atwho */ /*= require jquery.atwho */
/*= require jquery.scrollTo */ /*= require jquery.scrollTo */
/*= require jquery.turbolinks */ /*= require jquery.turbolinks */
/*= require js.cookie */
/*= require turbolinks */ /*= require turbolinks */
/*= require autosave */ /*= require autosave */
/*= require bootstrap/affix */ /*= require bootstrap/affix */
...@@ -124,15 +124,11 @@ ...@@ -124,15 +124,11 @@
return str.replace(/<(?:.|\n)*?>/gm, ''); return str.replace(/<(?:.|\n)*?>/gm, '');
}; };
window.unbindEvents = function() {
return $(document).off('scroll');
};
window.shiftWindow = function() { window.shiftWindow = function() {
return scrollBy(0, -100); return scrollBy(0, -100);
}; };
document.addEventListener("page:fetch", unbindEvents); document.addEventListener("page:fetch", gl.utils.cleanupBeforeFetch);
window.addEventListener("hashchange", shiftWindow); window.addEventListener("hashchange", shiftWindow);
...@@ -149,6 +145,10 @@ ...@@ -149,6 +145,10 @@
$document = $(document); $document = $(document);
$window = $(window); $window = $(window);
$body = $('body'); $body = $('body');
// Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/';
gl.utils.preventDisabledButtons(); gl.utils.preventDisabledButtons();
bootstrapBreakpoint = bp.getBreakpointSize(); bootstrapBreakpoint = bp.getBreakpointSize();
$(".nav-sidebar").niceScroll({ $(".nav-sidebar").niceScroll({
......
...@@ -322,21 +322,18 @@ ...@@ -322,21 +322,18 @@
var frequentlyUsedEmojis; var frequentlyUsedEmojis;
frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
frequentlyUsedEmojis.push(emoji); frequentlyUsedEmojis.push(emoji);
return $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { Cookies.set('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 });
path: gon.relative_url_root || '/',
expires: 365
});
}; };
AwardsHandler.prototype.getFrequentlyUsedEmojis = function() { AwardsHandler.prototype.getFrequentlyUsedEmojis = function() {
var frequentlyUsedEmojis; var frequentlyUsedEmojis;
frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(','); frequentlyUsedEmojis = (Cookies.get('frequently_used_emojis') || '').split(',');
return _.compact(_.uniq(frequentlyUsedEmojis)); return _.compact(_.uniq(frequentlyUsedEmojis));
}; };
AwardsHandler.prototype.renderFrequentlyUsedBlock = function() { AwardsHandler.prototype.renderFrequentlyUsedBlock = function() {
var emoji, frequentlyUsedEmojis, i, len, ul; var emoji, frequentlyUsedEmojis, i, len, ul;
if ($.cookie('frequently_used_emojis')) { if (Cookies.get('frequently_used_emojis')) {
frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>"); ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>");
for (i = 0, len = frequentlyUsedEmojis.length; i < len; i++) { for (i = 0, len = frequentlyUsedEmojis.length; i < len; i++) {
......
...@@ -68,14 +68,10 @@ ...@@ -68,14 +68,10 @@
// To be implemented on the extending class // To be implemented on the extending class
// e.g. // e.g.
// Api.gitignoreText item.name, @requestFileSuccess.bind(@) // Api.gitignoreText item.name, @requestFileSuccess.bind(@)
requestFileSuccess(file, { skipFocus, append } = {}) { requestFileSuccess(file, { skipFocus } = {}) {
const oldValue = this.editor.getValue(); const oldValue = this.editor.getValue();
let newValue = file.content; let newValue = file.content;
if (append && oldValue.length && oldValue !== newValue) {
newValue = oldValue + '\n\n' + newValue;
}
this.editor.setValue(newValue, 1); this.editor.setValue(newValue, 1);
if (!skipFocus) this.editor.focus(); if (!skipFocus) this.editor.focus();
...@@ -99,4 +95,3 @@ ...@@ -99,4 +95,3 @@
global.TemplateSelector = TemplateSelector; global.TemplateSelector = TemplateSelector;
})(window.gl || ( window.gl = {})); })(window.gl || ( window.gl = {}));
...@@ -5,7 +5,9 @@ ...@@ -5,7 +5,9 @@
//= require_tree ./stores //= require_tree ./stores
//= require_tree ./services //= require_tree ./services
//= require_tree ./mixins //= require_tree ./mixins
//= require_tree ./filters
//= require ./components/board //= require ./components/board
//= require ./components/board_sidebar
//= require ./components/new_list_dropdown //= require ./components/new_list_dropdown
//= require ./vue_resource_interceptor //= require ./vue_resource_interceptor
...@@ -22,7 +24,8 @@ $(() => { ...@@ -22,7 +24,8 @@ $(() => {
gl.IssueBoardsApp = new Vue({ gl.IssueBoardsApp = new Vue({
el: $boardApp, el: $boardApp,
components: { components: {
'board': gl.issueBoards.Board 'board': gl.issueBoards.Board,
'board-sidebar': gl.issueBoards.BoardSidebar
}, },
data: { data: {
state: Store.state, state: Store.state,
...@@ -30,9 +33,15 @@ $(() => { ...@@ -30,9 +33,15 @@ $(() => {
endpoint: $boardApp.dataset.endpoint, endpoint: $boardApp.dataset.endpoint,
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,
detailIssue: Store.detail
}, },
init: Store.create.bind(Store), init: Store.create.bind(Store),
computed: {
detailIssueVisible () {
return Object.keys(this.detailIssue.issue).length;
}
},
created () { created () {
gl.boardService = new BoardService(this.endpoint, this.boardId); gl.boardService = new BoardService(this.endpoint, this.boardId);
}, },
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
}, },
data () { data () {
return { return {
detailIssue: Store.detail,
filters: Store.state.filters, filters: Store.state.filters,
showIssueForm: false showIssueForm: false
}; };
...@@ -32,6 +33,26 @@ ...@@ -32,6 +33,26 @@
this.list.getIssues(true); this.list.getIssues(true);
}, },
deep: 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: { methods: {
......
...@@ -12,6 +12,17 @@ ...@@ -12,6 +12,17 @@
disabled: Boolean, disabled: Boolean,
index: Number index: Number
}, },
data () {
return {
showDetail: false,
detailIssue: Store.detail
};
},
computed: {
issueDetailVisible () {
return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
}
},
methods: { methods: {
filterByLabel (label, e) { filterByLabel (label, e) {
let labelToggleText = label.title; let labelToggleText = label.title;
...@@ -37,6 +48,29 @@ ...@@ -37,6 +48,29 @@
$('.labels-filter .dropdown-toggle-text').text(labelToggleText); $('.labels-filter .dropdown-toggle-text').text(labelToggleText);
Store.updateFiltersUrl(); 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;
}
}
} }
} }
}); });
......
(() => { (() => {
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.issueBoards.BoardNewIssue = Vue.extend({ gl.issueBoards.BoardNewIssue = Vue.extend({
...@@ -27,13 +29,16 @@ ...@@ -27,13 +29,16 @@
const labels = this.list.label ? [this.list.label] : []; const labels = this.list.label ? [this.list.label] : [];
const issue = new ListIssue({ const issue = new ListIssue({
title: this.title, title: this.title,
labels labels,
subscribed: true
}); });
this.list.newIssue(issue) this.list.newIssue(issue)
.then((data) => { .then((data) => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions // Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$els.submitButton).enable(); $(this.$els.submitButton).enable();
Store.detail.issue = issue;
}) })
.catch(() => { .catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions // Need this because our jQuery very kindly disables buttons on ALL form submissions
......
(() => {
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');
}
});
})();
Vue.filter('due-date', (value) => {
const date = new Date(value);
return $.datepicker.formatDate('M d, yy', date);
});
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
fallbackOnBody: true, fallbackOnBody: true,
ghostClass: 'is-ghost', ghostClass: 'is-ghost',
filter: '.has-tooltip, .btn', filter: '.has-tooltip, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 0, delay: gl.issueBoards.touchEnabled ? 100 : 50,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20, scrollSpeed: 20,
onStart: gl.issueBoards.onStart, onStart: gl.issueBoards.onStart,
......
...@@ -3,12 +3,18 @@ class ListIssue { ...@@ -3,12 +3,18 @@ class ListIssue {
this.id = obj.iid; this.id = obj.iid;
this.title = obj.title; this.title = obj.title;
this.confidential = obj.confidential; this.confidential = obj.confidential;
this.dueDate = obj.due_date;
this.subscribed = obj.subscribed;
this.labels = []; this.labels = [];
if (obj.assignee) { if (obj.assignee) {
this.assignee = new ListUser(obj.assignee); this.assignee = new ListUser(obj.assignee);
} }
if (obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
}
obj.labels.forEach((label) => { obj.labels.forEach((label) => {
this.labels.push(new ListLabel(label)); this.labels.push(new ListLabel(label));
}); });
...@@ -41,4 +47,21 @@ class ListIssue { ...@@ -41,4 +47,21 @@ class ListIssue {
getLists () { getLists () {
return gl.issueBoards.BoardsStore.state.lists.filter( list => list.findIssue(this.id) ); 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);
}
} }
class ListMilestone {
constructor (obj) {
this.id = obj.id;
this.title = obj.title;
}
}
class BoardService { class BoardService {
constructor (root, boardId) { constructor (root, boardId) {
Vue.http.options.root = root;
this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, { this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
generate: { generate: {
method: 'POST', method: 'POST',
......
...@@ -5,6 +5,9 @@ ...@@ -5,6 +5,9 @@
gl.issueBoards.BoardsStore = { gl.issueBoards.BoardsStore = {
disabled: false, disabled: false,
state: {}, state: {},
detail: {
issue: {}
},
moving: { moving: {
issue: {}, issue: {},
list: {} list: {}
...@@ -58,12 +61,12 @@ ...@@ -58,12 +61,12 @@
removeBlankState () { removeBlankState () {
this.removeList('blank'); this.removeList('blank');
$.cookie('issue_board_welcome_hidden', 'true', { Cookies.set('issue_board_welcome_hidden', 'true', {
expires: 365 * 10 expires: 365 * 10
}); });
}, },
welcomeIsHidden () { welcomeIsHidden () {
return $.cookie('issue_board_welcome_hidden') === 'true'; return Cookies.get('issue_board_welcome_hidden') === 'true';
}, },
removeList (id, type = 'blank') { removeList (id, type = 'blank') {
const list = this.findList('id', id, type); const list = this.findList('id', id, type);
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
const store = gl.cycleAnalyticsStore = { const store = gl.cycleAnalyticsStore = {
isLoading: true, isLoading: true,
hasError: false, hasError: false,
isHelpDismissed: $.cookie(COOKIE_NAME), isHelpDismissed: Cookies.get(COOKIE_NAME),
analytics: {} analytics: {}
}; };
...@@ -75,9 +75,7 @@ ...@@ -75,9 +75,7 @@
dismissLanding() { dismissLanding() {
store.isHelpDismissed = true; store.isHelpDismissed = true;
$.cookie(COOKIE_NAME, true, { Cookies.set(COOKIE_NAME, true);
path: gon.relative_url_root || '/'
});
} }
initDropdown() { initDropdown() {
......
...@@ -41,7 +41,12 @@ ...@@ -41,7 +41,12 @@
defaultDate: $("input[name='" + this.fieldName + "']").val(), defaultDate: $("input[name='" + this.fieldName + "']").val(),
altField: "input[name='" + this.fieldName + "']", altField: "input[name='" + this.fieldName + "']",
onSelect: () => { 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);
}
} }
}); });
} }
...@@ -49,8 +54,14 @@ ...@@ -49,8 +54,14 @@
initRemoveDueDate() { initRemoveDueDate() {
this.$block.on('click', '.js-remove-due-date', (e) => { this.$block.on('click', '.js-remove-due-date', (e) => {
e.preventDefault(); 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);
}
}); });
} }
...@@ -83,6 +94,18 @@ ...@@ -83,6 +94,18 @@
this.datePayload = datePayload; 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) { submitSelectedDate(isDropdown) {
return $.ajax({ return $.ajax({
type: 'PUT', type: 'PUT',
......
Element.prototype.matches = Element.prototype.matches || Element.prototype.msMatches;
Element.prototype.closest = function closest(selector, selectedElement = this) {
if (!selectedElement) return;
return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement);
};
...@@ -549,6 +549,8 @@ ...@@ -549,6 +549,8 @@
value = this.options.id ? this.options.id(data) : data.id; value = this.options.id ? this.options.id(data) : data.id;
fieldName = this.options.fieldName; fieldName = this.options.fieldName;
if (value) { value = value.toString().replace(/'/g, '\\\'') };
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
if (field.length) { if (field.length) {
selected = true; selected = true;
...@@ -620,6 +622,17 @@ ...@@ -620,6 +622,17 @@
selectedObject = this.renderedData[selectedIndex]; 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 = []; 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) { if (isInput) {
......
...@@ -15,16 +15,61 @@ ...@@ -15,16 +15,61 @@
return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>'); return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
}, },
initSearch: function() { initSearch: function() {
const $searchInput = $('#issuable_search');
Issuable.initSearchState($searchInput);
// `immediate` param set to false debounces on the `trailing` edge, lets user finish typing // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
const debouncedExecSearch = _.debounce(Issuable.executeSearch, 500, false); const debouncedExecSearch = _.debounce(Issuable.executeSearch, 1000, false);
$('#issuable_search').off('keyup').on('keyup', debouncedExecSearch); $searchInput.off('keyup').on('keyup', debouncedExecSearch);
// ensures existing filters are preserved when manually submitted // ensures existing filters are preserved when manually submitted
$('#issue_search_form').on('submit', (e) => { $('#issuable_search_form').on('submit', (e) => {
e.preventDefault(); e.preventDefault();
debouncedExecSearch(e); debouncedExecSearch(e);
}); });
},
initSearchState: function($searchInput) {
const currentSearchVal = $searchInput.val();
Issuable.searchState = {
elem: $searchInput,
current: currentSearchVal
};
Issuable.maybeFocusOnSearch();
},
accessSearchPristine: function(set) {
// store reference to previous value to prevent search on non-mutating keyup
const state = Issuable.searchState;
const currentSearchVal = state.elem.val();
if (set) {
state.current = currentSearchVal;
} else {
return state.current === currentSearchVal;
}
},
maybeFocusOnSearch: function() {
const currentSearchVal = Issuable.searchState.current;
if (currentSearchVal && currentSearchVal !== '') {
const queryLength = currentSearchVal.length;
const $searchInput = Issuable.searchState.elem;
/* The following ensures that the cursor is initially placed at
* the end of search input when focus is applied. It accounts
* for differences in browser implementations of `setSelectionRange`
* and cursor placement for elements in focus.
*/
$searchInput.focus();
if ($searchInput.setSelectionRange) {
$searchInput.setSelectionRange(queryLength, queryLength);
} else {
$searchInput.val(currentSearchVal);
}
}
}, },
executeSearch: function(e) { executeSearch: function(e) {
const $search = $('#issuable_search'); const $search = $('#issuable_search');
...@@ -32,6 +77,11 @@ ...@@ -32,6 +77,11 @@
const $searchValue = $search.val(); const $searchValue = $search.val();
const $filtersForm = $('.js-filter-form'); const $filtersForm = $('.js-filter-form');
const $input = $(`input[name='${$searchName}']`, $filtersForm); const $input = $(`input[name='${$searchName}']`, $filtersForm);
const isPristine = Issuable.accessSearchPristine();
if (isPristine) {
return;
}
if (!$input.length) { if (!$input.length) {
$filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`); $filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`);
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
abilityName = $dropdown.data('ability-name'); abilityName = $dropdown.data('ability-name');
$selectbox = $dropdown.closest('.selectbox'); $selectbox = $dropdown.closest('.selectbox');
$block = $selectbox.closest('.block'); $block = $selectbox.closest('.block');
$form = $dropdown.closest('form'); $form = $dropdown.closest('form, .js-issuable-update');
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span'); $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
$sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip'); $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
$value = $block.find('.value'); $value = $block.find('.value');
...@@ -317,6 +317,7 @@ ...@@ -317,6 +317,7 @@
} }
}, },
multiSelect: $dropdown.hasClass('js-multiselect'), multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(label, $el, e) { clicked: function(label, $el, e) {
var isIssueIndex, isMRIndex, page; var isIssueIndex, isMRIndex, page;
_this.enableBulkLabelDropdown(); _this.enableBulkLabelDropdown();
...@@ -334,7 +335,7 @@ ...@@ -334,7 +335,7 @@
page = $('body').data('page'); page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests: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) { if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = []; gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
} }
...@@ -362,6 +363,30 @@ ...@@ -362,6 +363,30 @@
else if ($dropdown.hasClass('js-filter-submit')) { else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').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 { else {
if ($dropdown.hasClass('js-multiselect')) { if ($dropdown.hasClass('js-multiselect')) {
......
...@@ -43,6 +43,14 @@ ...@@ -43,6 +43,14 @@
parser.href = url; parser.href = url;
return parser; return parser;
}; };
gl.utils.cleanupBeforeFetch = function() {
// Unbind scroll events
$(document).off('scroll');
// Close any open tooltips
$('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
};
return jQuery.timefor = function(time, suffix, expiredLabel) { return jQuery.timefor = function(time, suffix, expiredLabel) {
var suffixFromNow, timefor; var suffixFromNow, timefor;
if (!time) { if (!time) {
......
((global) => { ((global) => {
global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts = global.mergeConflicts || {};
const diffViewType = $.cookie('diff_view'); const diffViewType = Cookies.get('diff_view');
const HEAD_HEADER_TEXT = 'HEAD//our changes'; const HEAD_HEADER_TEXT = 'HEAD//our changes';
const ORIGIN_HEADER_TEXT = 'origin//their changes'; const ORIGIN_HEADER_TEXT = 'origin//their changes';
const HEAD_BUTTON_TITLE = 'Use ours'; const HEAD_BUTTON_TITLE = 'Use ours';
...@@ -180,9 +180,7 @@ ...@@ -180,9 +180,7 @@
this.state.diffView = viewType; this.state.diffView = viewType;
this.state.isParallel = viewType === VIEW_TYPES.PARALLEL; this.state.isParallel = viewType === VIEW_TYPES.PARALLEL;
$.cookie('diff_view', viewType, { Cookies.set('diff_view', viewType);
path: gon.relative_url_root || '/'
});
}, },
getHeadHeaderLine(id) { getHeadHeaderLine(id) {
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
// Handles persisting and restoring the current tab selection and lazily-loading // Handles persisting and restoring the current tab selection and lazily-loading
// content on the MergeRequests#show page. // content on the MergeRequests#show page.
// //
/*= require jquery.cookie */ /*= require js.cookie */
// //
// ### Example Markup // ### Example Markup
...@@ -368,7 +368,7 @@ ...@@ -368,7 +368,7 @@
MergeRequestTabs.prototype.expandView = function() { MergeRequestTabs.prototype.expandView = function() {
var $gutterIcon; var $gutterIcon;
if ($.cookie('collapsed_gutter') === 'true') { if (Cookies.get('collapsed_gutter') === 'true') {
return; return;
} }
$gutterIcon = $('.js-sidebar-toggle i:visible'); $gutterIcon = $('.js-sidebar-toggle i:visible');
......
...@@ -101,6 +101,7 @@ ...@@ -101,6 +101,7 @@
// display:block overrides the hide-collapse rule // display:block overrides the hide-collapse rule
return $value.css('display', ''); return $value.css('display', '');
}, },
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(selected, $el, e) { clicked: function(selected, $el, e) {
var data, isIssueIndex, isMRIndex, page; var data, isIssueIndex, isMRIndex, page;
page = $('body').data('page'); page = $('body').data('page');
...@@ -110,7 +111,7 @@ ...@@ -110,7 +111,7 @@
e.preventDefault(); e.preventDefault();
return; 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.state.filters[$dropdown.data('field-name')] = selected.name;
gl.issueBoards.BoardsStore.updateFiltersUrl(); gl.issueBoards.BoardsStore.updateFiltersUrl();
e.preventDefault(); e.preventDefault();
...@@ -123,6 +124,24 @@ ...@@ -123,6 +124,24 @@
return Issuable.filterResults($dropdown.closest('form')); return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) { } else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').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 { } else {
selected = $selectbox.find('input[type="hidden"]').val(); selected = $selectbox.find('input[type="hidden"]').val();
data = {}; data = {};
......
...@@ -2,36 +2,39 @@ ...@@ -2,36 +2,39 @@
class Pipelines { class Pipelines {
constructor() { constructor() {
$(document).off('click', '.toggle-pipeline-btn').on('click', '.toggle-pipeline-btn', this.toggleGraph); this.initGraphToggle();
this.addMarginToBuildColumns(); this.addMarginToBuildColumns();
} }
toggleGraph() { initGraphToggle() {
const $pipelineBtn = $(this).closest('.toggle-pipeline-btn'); this.pipelineGraph = document.querySelector('.pipeline-graph');
const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph'); this.toggleButton = document.querySelector('.toggle-pipeline-btn');
const $btnText = $(this).find('.toggle-btn-text'); this.toggleButtonText = this.toggleButton.querySelector('.toggle-btn-text');
const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed'); this.toggleButton.addEventListener('click', this.toggleGraph.bind(this));
}
$($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
graphCollapsed ? $btnText.text('Hide') : $btnText.text('Expand') toggleGraph() {
const graphCollapsed = this.pipelineGraph.classList.contains('graph-collapsed');
this.toggleButton.classList.toggle('graph-collapsed');
this.pipelineGraph.classList.toggle('graph-collapsed');
this.toggleButtonText.textContent = graphCollapsed ? 'Hide' : 'Expand';
} }
addMarginToBuildColumns() { addMarginToBuildColumns() {
const $secondChildBuildNode = $('.build:nth-child(2)'); const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)');
if ($secondChildBuildNode.length) { for (buildNodeIndex in secondChildBuildNodes) {
const $firstChildBuildNode = $secondChildBuildNode.prev('.build'); const buildNode = secondChildBuildNodes[buildNodeIndex];
const $multiBuildColumn = $secondChildBuildNode.closest('.stage-column'); const firstChildBuildNode = buildNode.previousElementSibling;
const $previousColumn = $multiBuildColumn.prev('.stage-column'); if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue;
$multiBuildColumn.addClass('left-margin'); const multiBuildColumn = buildNode.closest('.stage-column');
$firstChildBuildNode.addClass('left-connector'); const previousColumn = multiBuildColumn.previousElementSibling;
$previousColumn.each(function() { if (!previousColumn || !previousColumn.matches('.stage-column')) continue;
$this = $(this); multiBuildColumn.classList.add('left-margin');
if ($('.build', $this).length === 1) $this.addClass('no-margin'); firstChildBuildNode.classList.add('left-connector');
}); const columnBuilds = previousColumn.querySelectorAll('.build');
if (columnBuilds.length === 1) previousColumn.classList.add('no-margin');
} }
$('.pipeline-graph').removeClass('hidden'); this.pipelineGraph.classList.remove('hidden');
} }
} }
......
...@@ -23,16 +23,12 @@ ...@@ -23,16 +23,12 @@
return $(this).parents('form').submit(); return $(this).parents('form').submit();
}); });
$('.hide-no-ssh-message').on('click', function(e) { $('.hide-no-ssh-message').on('click', function(e) {
$.cookie('hide_no_ssh_message', 'false', { Cookies.set('hide_no_ssh_message', 'false');
path: gon.relative_url_root || '/'
});
$(this).parents('.no-ssh-key-message').remove(); $(this).parents('.no-ssh-key-message').remove();
return e.preventDefault(); return e.preventDefault();
}); });
$('.hide-no-password-message').on('click', function(e) { $('.hide-no-password-message').on('click', function(e) {
$.cookie('hide_no_password_message', 'false', { Cookies.set('hide_no_password_message', 'false');
path: gon.relative_url_root || '/'
});
$(this).parents('.no-password-message').remove(); $(this).parents('.no-password-message').remove();
return e.preventDefault(); return e.preventDefault();
}); });
...@@ -82,7 +78,7 @@ ...@@ -82,7 +78,7 @@
if (ref.header != null) { if (ref.header != null) {
return $('<li />').addClass('dropdown-header').text(ref.header); return $('<li />').addClass('dropdown-header').text(ref.header);
} else { } 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); return $('<li />').append(link);
} }
}, },
......
...@@ -5,15 +5,24 @@ ...@@ -5,15 +5,24 @@
function Sidebar(currentUser) { function Sidebar(currentUser) {
this.toggleTodo = bind(this.toggleTodo, this); this.toggleTodo = bind(this.toggleTodo, this);
this.sidebar = $('aside'); this.sidebar = $('aside');
this.removeListeners();
this.addEventListeners(); 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() { Sidebar.prototype.addEventListeners = function() {
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); $('.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; var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault(); e.preventDefault();
$this = $(this); $this = $(this);
...@@ -29,9 +38,7 @@ ...@@ -29,9 +38,7 @@
$('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); $('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
} }
if (!triggered) { if (!triggered) {
return $.cookie("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'), { return Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
path: gon.relative_url_root || '/'
});
} }
}); });
return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo); return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo);
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
} }
init() { init() {
this.isPinned = $.cookie(pinnedStateCookie) === 'true'; this.isPinned = Cookies.get(pinnedStateCookie) === 'true';
this.isExpanded = ( this.isExpanded = (
window.innerWidth >= sidebarBreakpoint && window.innerWidth >= sidebarBreakpoint &&
$(pageSelector).hasClass(expandedPageClass) $(pageSelector).hasClass(expandedPageClass)
...@@ -62,10 +62,7 @@ ...@@ -62,10 +62,7 @@
if (!this.isPinned) { if (!this.isPinned) {
this.isExpanded = false; this.isExpanded = false;
} }
$.cookie(pinnedStateCookie, this.isPinned ? 'true' : 'false', { Cookies.set(pinnedStateCookie, this.isPinned ? 'true' : 'false', { expires: 3650 });
path: gon.relative_url_root || '/',
expires: 3650
});
this.renderState(); this.renderState();
} }
......
...@@ -5,10 +5,10 @@ ...@@ -5,10 +5,10 @@
function Subscription(container) { function Subscription(container) {
this.toggleSubscription = bind(this.toggleSubscription, this); this.toggleSubscription = bind(this.toggleSubscription, this);
var $container; var $container;
$container = $(container); this.$container = $(container);
this.url = $container.attr('data-url'); this.url = this.$container.attr('data-url');
this.subscribe_button = $container.find('.js-subscribe-button'); this.subscribe_button = this.$container.find('.js-subscribe-button');
this.subscription_status = $container.find('.subscription-status'); this.subscription_status = this.$container.find('.subscription-status');
this.subscribe_button.unbind('click').click(this.toggleSubscription); this.subscribe_button.unbind('click').click(this.toggleSubscription);
} }
...@@ -18,17 +18,27 @@ ...@@ -18,17 +18,27 @@
action = btn.find('span').text(); action = btn.find('span').text();
current_status = this.subscription_status.attr('data-status'); current_status = this.subscription_status.attr('data-status');
btn.addClass('disabled'); btn.addClass('disabled');
if ($('html').hasClass('issue-boards-page')) {
this.url = this.$container.attr('data-url');
}
return $.post(this.url, (function(_this) { return $.post(this.url, (function(_this) {
return function() { return function() {
var status; var status;
btn.removeClass('disabled'); btn.removeClass('disabled');
status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed';
_this.subscription_status.attr('data-status', status); if ($('html').hasClass('issue-boards-page')) {
action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe'; Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'subscribed', !gl.issueBoards.BoardsStore.detail.issue.subscribed);
btn.find('span').text(action); } else {
_this.subscription_status.find('>div').toggleClass('hidden'); status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed';
if (btn.attr('data-original-title')) { _this.subscription_status.attr('data-status', status);
return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle'); 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)); })(this));
......
...@@ -32,24 +32,22 @@ ...@@ -32,24 +32,22 @@
this.currentTemplate = currentTemplate; this.currentTemplate = currentTemplate;
if (err) return; // Error handled by global AJAX error handler if (err) return; // Error handled by global AJAX error handler
this.stopLoadingSpinner(); this.stopLoadingSpinner();
this.setInputValueToTemplateContent(true); this.setInputValueToTemplateContent();
}); });
return; return;
} }
setInputValueToTemplateContent(append) { setInputValueToTemplateContent() {
// `this.requestFileSuccess` sets the value of the description input field // `this.requestFileSuccess` sets the value of the description input field
// to the content of the template selected. If `append` is true, the // to the content of the template selected.
// template content will be appended to the previous value of the field,
// separated by a blank line if the previous value is non-empty.
if (this.titleInput.val() === '') { if (this.titleInput.val() === '') {
// If the title has not yet been set, focus the title input and // If the title has not yet been set, focus the title input and
// skip focusing the description input by setting `true` as the // skip focusing the description input by setting `true` as the
// `skipFocus` option to `requestFileSuccess`. // `skipFocus` option to `requestFileSuccess`.
this.requestFileSuccess(this.currentTemplate, {skipFocus: true, append}); this.requestFileSuccess(this.currentTemplate, {skipFocus: true});
this.titleInput.focus(); this.titleInput.focus();
} else { } else {
this.requestFileSuccess(this.currentTemplate, {skipFocus: false, append}); this.requestFileSuccess(this.currentTemplate, {skipFocus: false});
} }
return; return;
} }
......
...@@ -23,10 +23,7 @@ ...@@ -23,10 +23,7 @@
hideProjectLimitMessage() { hideProjectLimitMessage() {
$('.hide-project-limit-message').on('click', e => { $('.hide-project-limit-message').on('click', e => {
e.preventDefault(); e.preventDefault();
const path = gon.relative_url_root || '/'; Cookies.set('hide_project_limit_message', 'false');
$.cookie('hide_project_limit_message', 'false', {
path: path
});
$(this).parents('.project-limit-message').remove(); $(this).parents('.project-limit-message').remove();
}); });
} }
......
...@@ -9,7 +9,11 @@ ...@@ -9,7 +9,11 @@
this.usersPath = "/autocomplete/users.json"; this.usersPath = "/autocomplete/users.json";
this.userPath = "/autocomplete/users/:id.json"; this.userPath = "/autocomplete/users/:id.json";
if (currentUser != null) { 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) { $('.js-user-search').each((function(_this) {
return function(i, dropdown) { return function(i, dropdown) {
...@@ -32,9 +36,30 @@ ...@@ -32,9 +36,30 @@
$value = $block.find('.value'); $value = $block.find('.value');
$collapsedSidebar = $block.find('.sidebar-collapsed-user'); $collapsedSidebar = $block.find('.sidebar-collapsed-user');
$loading = $block.find('.block-loading').fadeOut(); $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) { $block.on('click', '.js-assign-yourself', function(e) {
e.preventDefault(); 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) { assignTo = function(selected) {
var data; var data;
...@@ -150,6 +175,7 @@ ...@@ -150,6 +175,7 @@
// display:block overrides the hide-collapse rule // display:block overrides the hide-collapse rule
return $value.css('display', ''); return $value.css('display', '');
}, },
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(user, $el, e) { clicked: function(user, $el, e) {
var isIssueIndex, isMRIndex, page, selected; var isIssueIndex, isMRIndex, page, selected;
page = $('body').data('page'); page = $('body').data('page');
...@@ -160,7 +186,7 @@ ...@@ -160,7 +186,7 @@
selectedId = user.id; selectedId = user.id;
return; return;
} }
if ($('html').hasClass('issue-boards-page')) { if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
selectedId = user.id; selectedId = user.id;
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id; gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
gl.issueBoards.BoardsStore.updateFiltersUrl(); gl.issueBoards.BoardsStore.updateFiltersUrl();
...@@ -170,6 +196,19 @@ ...@@ -170,6 +196,19 @@
return Issuable.filterResults($dropdown.closest('form')); return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) { } else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').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 { } else {
selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val(); selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val();
return assignTo(selected); return assignTo(selected);
......
...@@ -227,7 +227,7 @@ header { ...@@ -227,7 +227,7 @@ header {
float: none !important; float: none !important;
.visible-xs, .visible-xs,
.visable-sm { .visible-sm {
display: table-cell !important; display: table-cell !important;
} }
} }
......
...@@ -45,6 +45,15 @@ ...@@ -45,6 +45,15 @@
.page-with-sidebar { .page-with-sidebar {
padding-bottom: 0; padding-bottom: 0;
} }
.issues-filters {
position: relative;
z-index: 999999;
}
}
.boards-app {
position: relative;
} }
.boards-app-loading { .boards-app-loading {
...@@ -66,6 +75,10 @@ ...@@ -66,6 +75,10 @@
height: 475px; // Needed for PhantomJS height: 475px; // Needed for PhantomJS
height: calc(100vh - 220px); height: calc(100vh - 220px);
min-height: 475px; min-height: 475px;
&.is-compact {
width: calc(100% - 290px);
}
} }
} }
...@@ -184,6 +197,10 @@ ...@@ -184,6 +197,10 @@
margin-bottom: 5px; margin-bottom: 5px;
} }
&.is-active {
background-color: $row-hover;
}
.label { .label {
border: 0; border: 0;
outline: 0; outline: 0;
...@@ -212,6 +229,10 @@ ...@@ -212,6 +229,10 @@
margin-right: 5px; margin-right: 5px;
font-size: (14px / $issue-boards-font-size) * 1em; font-size: (14px / $issue-boards-font-size) * 1em;
} }
.avatar {
margin-left: 0;
}
} }
.card-number { .card-number {
...@@ -264,3 +285,48 @@ ...@@ -264,3 +285,48 @@
border-width: 1px 0 1px 1px; 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;
}
}
}
...@@ -73,10 +73,13 @@ module Projects ...@@ -73,10 +73,13 @@ module Projects
def serialize_as_json(resource) def serialize_as_json(resource)
resource.as_json( resource.as_json(
labels: true, labels: true,
only: [:iid, :title, :confidential], only: [:iid, :title, :confidential, :due_date],
include: { 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 end
end end
......
...@@ -50,7 +50,7 @@ class LabelsFinder < UnionFinder ...@@ -50,7 +50,7 @@ class LabelsFinder < UnionFinder
end end
def projects_ids def projects_ids
params[:project_ids].presence params[:project_ids]
end end
def title def title
......
...@@ -5,7 +5,7 @@ module SidekiqHelper ...@@ -5,7 +5,7 @@ module SidekiqHelper
(?<mem>[\d\.,]+)\s+ (?<mem>[\d\.,]+)\s+
(?<state>[DRSTWXZNLsl\+<]+)\s+ (?<state>[DRSTWXZNLsl\+<]+)\s+
(?<start>.+)\s+ (?<start>.+)\s+
(?<command>sidekiq.*\])\s+ (?<command>sidekiq.*\])\s*
\z/x \z/x
def parse_sidekiq_ps(line) def parse_sidekiq_ps(line)
......
...@@ -287,10 +287,12 @@ class Issue < ActiveRecord::Base ...@@ -287,10 +287,12 @@ class Issue < ActiveRecord::Base
def as_json(options = {}) def as_json(options = {})
super(options).tap do |json| super(options).tap do |json|
json[:subscribed] = subscribed?(options[:user]) if options.has_key?(:user)
if options.has_key?(:labels) if options.has_key?(:labels)
json[:labels] = labels.as_json( json[:labels] = labels.as_json(
project: project, project: project,
only: [:id, :title, :description, :color], only: [:id, :title, :description, :color, :priority],
methods: [:text_color] methods: [:text_color]
) )
end end
......
...@@ -461,7 +461,7 @@ ...@@ -461,7 +461,7 @@
.panel-body .panel-body
= lorem = lorem
%h2#alert Alerts %h2#alerts Alerts
.row .row
.col-md-6 .col-md-6
......
...@@ -7,8 +7,11 @@ ...@@ -7,8 +7,11 @@
":issue-link-base" => "issueLinkBase", ":issue-link-base" => "issueLinkBase",
":disabled" => "disabled", ":disabled" => "disabled",
"track-by" => "id" } "track-by" => "id" }
%li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id }", %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }",
":index" => "index" } ":index" => "index",
"@mousedown" => "mouseDown",
"@mouseMove" => "mouseMove",
"@mouseup" => "showIssue($event)" }
%h4.card-title %h4.card-title
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential") = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
%a{ ":href" => "issueLinkBase + '/' + issue.id", %a{ ":href" => "issueLinkBase + '/' + issue.id",
...@@ -18,6 +21,11 @@ ...@@ -18,6 +21,11 @@
%span.card-number{ "v-if" => "issue.id" } %span.card-number{ "v-if" => "issue.id" }
= precede '#' do = precede '#' do
{{ issue.id }} {{ 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", %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
type: "button", type: "button",
"v-if" => "(!list.label || label.id !== list.label.id)", "v-if" => "(!list.label || label.id !== list.label.id)",
...@@ -26,8 +34,3 @@ ...@@ -26,8 +34,3 @@
":title" => "label.description", ":title" => "label.description",
data: { container: 'body' } } data: { container: 'body' } }
{{ label.title }} {{ 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 }
%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"
.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
.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
.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"
.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
- 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.
...@@ -10,7 +10,9 @@ ...@@ -10,7 +10,9 @@
= render 'shared/issuable/filter', type: :boards = render 'shared/issuable/filter', type: :boards
.boards-list#board-app{ "v-cloak" => true, data: board_data } #board-app.boards-app{ "v-cloak" => true, data: board_data }
.boards-app-loading.text-center{ "v-if" => "loading" } .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
= icon("spinner spin") .boards-app-loading.text-center{ "v-if" => "loading" }
= render "projects/boards/components/board" = icon("spinner spin")
= render "projects/boards/components/board"
= render "projects/boards/components/sidebar"
...@@ -10,7 +10,9 @@ ...@@ -10,7 +10,9 @@
= render 'shared/issuable/filter', type: :boards = render 'shared/issuable/filter', type: :boards
.boards-list#board-app{ "v-cloak" => true, data: board_data } #board-app.boards-app{ "v-cloak" => true, data: board_data }
.boards-app-loading.text-center{ "v-if" => "loading" } .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
= icon("spinner spin") .boards-app-loading.text-center{ "v-if" => "loading" }
= render "projects/boards/components/board" = icon("spinner spin")
= render "projects/boards/components/board"
= render "projects/boards/components/sidebar"
<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
...@@ -9,6 +9,18 @@ class ProjectCacheWorker ...@@ -9,6 +9,18 @@ class ProjectCacheWorker
LEASE_TIMEOUT = 15.minutes.to_i LEASE_TIMEOUT = 15.minutes.to_i
def self.lease_for(project_id)
Gitlab::ExclusiveLease.
new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT)
end
# Overwrite Sidekiq's implementation so we only schedule when actually needed.
def self.perform_async(project_id)
# If a lease for this project is still being held there's no point in
# scheduling a new job.
super unless lease_for(project_id).exists?
end
def perform(project_id) def perform(project_id)
if try_obtain_lease_for(project_id) if try_obtain_lease_for(project_id)
Rails.logger. Rails.logger.
...@@ -37,8 +49,6 @@ class ProjectCacheWorker ...@@ -37,8 +49,6 @@ class ProjectCacheWorker
end end
def try_obtain_lease_for(project_id) def try_obtain_lease_for(project_id)
Gitlab::ExclusiveLease. self.class.lease_for(project_id).try_obtain
new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT).
try_obtain
end end
end end
...@@ -8,6 +8,8 @@ ...@@ -8,6 +8,8 @@
## Styleguides ## Styleguides
- [API styleguide](api_styleguide.md) Use this styleguide if you are
contributing to the API.
- [Documentation styleguide](doc_styleguide.md) Use this styleguide if you are - [Documentation styleguide](doc_styleguide.md) Use this styleguide if you are
contributing to documentation. contributing to documentation.
- [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations - [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations
......
# API styleguide
This styleguide recommends best practices for API development.
## Instance variables
Please do not use instance variables, there is no need for them (we don't need
to access them as we do in Rails views), local variables are fine.
## Entities
Always use an [Entity] to present the endpoint's payload.
## Methods and parameters description
Every method must be described using the [Grape DSL](https://github.com/ruby-grape/grape#describing-methods)
(see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/environments.rb
for a good example):
- `desc` for the method summary. You should pass it a block for additional
details such as:
- The GitLab version when the endpoint was added
- If the endpoint is deprecated, and if so, when will it be removed
- `params` for the method params. This acts as description,
[validation, and coercion of the parameters]
A good example is as follows:
```ruby
desc 'Get all broadcast messages' do
detail 'This feature was introduced in GitLab 8.12.'
success Entities::BroadcastMessage
end
params do
optional :page, type: Integer, desc: 'Current page number'
optional :per_page, type: Integer, desc: 'Number of messages per page'
end
get do
messages = BroadcastMessage.all
present paginate(messages), with: Entities::BroadcastMessage
end
```
## Declared params
> Grape allows you to access only the parameters that have been declared by your
`params` block. It filters out the params that have been passed, but are not
allowed.
– https://github.com/ruby-grape/grape#declared
### Exclude params from parent namespaces!
> By default `declared(params) `includes parameters that were defined in all
parent namespaces.
– https://github.com/ruby-grape/grape#include-parent-namespaces
In most cases you will want to exclude params from the parent namespaces:
```ruby
declared(params, include_parent_namespaces: false)
```
### When to use `declared(params)`?
You should always use `declared(params)` when you pass the params hash as
arguments to a method call.
For instance:
```ruby
# bad
User.create(params) # imagine the user submitted `admin=1`... :)
# good
User.create(declared(params, include_parent_namespaces: false).to_h)
```
>**Note:**
`declared(params)` return a `Hashie::Mash` object, on which you will have to
call `.to_h`.
But we can use `params[key]` directly when we access single elements.
For instance:
```ruby
# good
Model.create(foo: params[:foo])
```
[Entity]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/entities.rb
[validation, and coercion of the parameters]: https://github.com/ruby-grape/grape#parameter-validation-and-coercion
...@@ -342,12 +342,6 @@ You can use the following fake tokens as examples. ...@@ -342,12 +342,6 @@ You can use the following fake tokens as examples.
Here is a list of must-have items. Use them in the exact order that appears Here is a list of must-have items. Use them in the exact order that appears
on this document. Further explanation is given below. on this document. Further explanation is given below.
- Every method must be described using [Grape's DSL](https://github.com/ruby-grape/grape/tree/v0.13.0#describing-methods)
(see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/environments.rb
for a good example):
- `desc` for the method summary (you can pass it a block for additional details)
- `params` for the method params (this acts as description **and** validation
of the params)
- Every method must have the REST API request. For example: - Every method must have the REST API request. For example:
``` ```
......
...@@ -142,6 +142,9 @@ gitlab-workhorse we need a Go compiler. The instructions below assume you ...@@ -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 use 64-bit Linux. You can find downloads for other platforms at the [Go download
page](https://golang.org/dl). 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 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 - && \ 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 sudo tar -C /usr/local -xzf go1.5.3.linux-amd64.tar.gz
......
...@@ -85,8 +85,11 @@ Deleting old backups... [SKIPPING] ...@@ -85,8 +85,11 @@ Deleting old backups... [SKIPPING]
Starting with GitLab 7.4 you can let the backup script upload the '.tar' file it creates. Starting with GitLab 7.4 you can let the backup script upload the '.tar' file it creates.
It uses the [Fog library](http://fog.io/) to perform the upload. It uses the [Fog library](http://fog.io/) to perform the upload.
In the example below we use Amazon S3 for storage. In the example below we use Amazon S3 for storage, but Fog also lets you use
Fog also supports [other storage providers](http://fog.io/storage/). [other storage providers](http://fog.io/storage/). GitLab
[imports cloud drivers](https://gitlab.com/gitlab-org/gitlab-ce/blob/30f5b9a5b711b46f1065baf755e413ceced5646b/Gemfile#L88)
for AWS, Azure, Google, OpenStack Swift and Rackspace as well. A local driver is
[also available](#uploading-to-locally-mounted-shares).
For omnibus packages: For omnibus packages:
......
...@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-13-stable-ee ...@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-13-stable-ee
```bash ```bash
cd /home/git/gitlab-shell cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags sudo -u git -H git fetch --all --tags
sudo -u git -H git checkout v3.6.3 sudo -u git -H git checkout v3.6.6
``` ```
### 6. Update gitlab-workhorse ### 6. Update gitlab-workhorse
......
...@@ -49,18 +49,23 @@ module API ...@@ -49,18 +49,23 @@ module API
attrs = attributes_for_keys [:title, :key] attrs = attributes_for_keys [:title, :key]
attrs[:key].strip! if attrs[: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]) 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 # Check for available deploy keys in other projects
key = current_user.accessible_deploy_keys.find_by(key: attrs[:key]) key = current_user.accessible_deploy_keys.find_by(key: attrs[:key])
if key if key
user_project.deploy_keys << key user_project.deploy_keys << key
present key, with: Entities::SSHKey present key, with: Entities::SSHKey
break
end end
# Create a new deploy key
key = DeployKey.new attrs key = DeployKey.new attrs
if key.valid? && user_project.deploy_keys << key if key.valid? && user_project.deploy_keys << key
present key, with: Entities::SSHKey present key, with: Entities::SSHKey
else else
......
...@@ -4,25 +4,24 @@ module API ...@@ -4,25 +4,24 @@ module API
before { authenticate! } before { authenticate! }
before { authorize! :download_code, user_project } before { authorize! :download_code, user_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects do resource :projects do
# Get a project repository tags desc 'Get a project repository tags' do
# success Entities::RepoTag
# Parameters: end
# id (required) - The ID of a project
# Example Request:
# GET /projects/:id/repository/tags
get ":id/repository/tags" do get ":id/repository/tags" do
present user_project.repository.tags.sort_by(&:name).reverse, present user_project.repository.tags.sort_by(&:name).reverse,
with: Entities::RepoTag, project: user_project with: Entities::RepoTag, project: user_project
end end
# Get a single repository tag desc 'Get a single repository tag' do
# success Entities::RepoTag
# Parameters: end
# id (required) - The ID of a project params do
# tag_name (required) - The name of the tag requires :tag_name, type: String, desc: 'The name of the tag'
# Example Request: end
# GET /projects/:id/repository/tags/:tag_name
get ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do get ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do
tag = user_project.repository.find_tag(params[:tag_name]) tag = user_project.repository.find_tag(params[:tag_name])
not_found!('Tag') unless tag not_found!('Tag') unless tag
...@@ -30,20 +29,21 @@ module API ...@@ -30,20 +29,21 @@ module API
present tag, with: Entities::RepoTag, project: user_project present tag, with: Entities::RepoTag, project: user_project
end end
# Create tag desc 'Create a new repository tag' do
# success Entities::RepoTag
# Parameters: end
# id (required) - The ID of a project params do
# tag_name (required) - The name of the tag requires :tag_name, type: String, desc: 'The name of the tag'
# ref (required) - Create tag from commit sha or branch requires :ref, type: String, desc: 'The commit sha or branch name'
# message (optional) - Specifying a message creates an annotated tag. optional :message, type: String, desc: 'Specifying a message creates an annotated tag'
# Example Request: optional :release_description, type: String, desc: 'Specifying release notes stored in the GitLab database'
# POST /projects/:id/repository/tags end
post ':id/repository/tags' do post ':id/repository/tags' do
authorize_push_project authorize_push_project
message = params[:message] || nil create_params = declared(params)
result = CreateTagService.new(user_project, current_user). 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 if result[:status] == :success
present result[:tag], present result[:tag],
...@@ -54,15 +54,13 @@ module API ...@@ -54,15 +54,13 @@ module API
end end
end end
# Delete tag desc 'Delete a repository tag'
# params do
# Parameters: requires :tag_name, type: String, desc: 'The name of the tag'
# id (required) - The ID of a project end
# tag_name (required) - The name of the tag
# Example Request:
# DELETE /projects/:id/repository/tags/:tag
delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do
authorize_push_project authorize_push_project
result = DeleteTagService.new(user_project, current_user). result = DeleteTagService.new(user_project, current_user).
execute(params[:tag_name]) execute(params[:tag_name])
...@@ -75,17 +73,16 @@ module API ...@@ -75,17 +73,16 @@ module API
end end
end end
# Add release notes to tag desc 'Add a release note to a tag' do
# success Entities::Release
# Parameters: end
# id (required) - The ID of a project params do
# tag_name (required) - The name of the tag requires :tag_name, type: String, desc: 'The name of the tag'
# description (required) - Release notes with markdown support requires :description, type: String, desc: 'Release notes with markdown support'
# Example Request: end
# POST /projects/:id/repository/tags/:tag_name/release
post ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do post ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do
authorize_push_project authorize_push_project
required_attributes! [:description]
result = CreateReleaseService.new(user_project, current_user). result = CreateReleaseService.new(user_project, current_user).
execute(params[:tag_name], params[:description]) execute(params[:tag_name], params[:description])
...@@ -96,17 +93,16 @@ module API ...@@ -96,17 +93,16 @@ module API
end end
end end
# Updates a release notes of a tag desc "Update a tag's release note" do
# success Entities::Release
# Parameters: end
# id (required) - The ID of a project params do
# tag_name (required) - The name of the tag requires :tag_name, type: String, desc: 'The name of the tag'
# description (required) - Release notes with markdown support requires :description, type: String, desc: 'Release notes with markdown support'
# Example Request: end
# PUT /projects/:id/repository/tags/:tag_name/release
put ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do put ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do
authorize_push_project authorize_push_project
required_attributes! [:description]
result = UpdateReleaseService.new(user_project, current_user). result = UpdateReleaseService.new(user_project, current_user).
execute(params[:tag_name], params[:description]) execute(params[:tag_name], params[:description])
......
...@@ -333,11 +333,11 @@ module API ...@@ -333,11 +333,11 @@ module API
user = User.find_by(id: declared(params).id) user = User.find_by(id: declared(params).id)
not_found!('User') unless user not_found!('User') unless user
events = user.recent_events. events = user.events.
merge(ProjectsFinder.new.execute(current_user)). merge(ProjectsFinder.new.execute(current_user)).
references(:project). references(:project).
with_associations. with_associations.
page(params[:page]) recent
present paginate(events), with: Entities::Event present paginate(events), with: Entities::Event
end end
......
...@@ -27,7 +27,7 @@ module Gitlab ...@@ -27,7 +27,7 @@ module Gitlab
# on begin/ensure blocks to cancel a lease, because the 'ensure' does # on begin/ensure blocks to cancel a lease, because the 'ensure' does
# not always run. Think of 'kill -9' from the Unicorn master for # not always run. Think of 'kill -9' from the Unicorn master for
# instance. # instance.
# #
# If you find that leases are getting in your way, ask yourself: would # If you find that leases are getting in your way, ask yourself: would
# it be enough to lower the lease timeout? Another thing that might be # it be enough to lower the lease timeout? Another thing that might be
# appropriate is to only use a lease for bulk/automated operations, and # appropriate is to only use a lease for bulk/automated operations, and
...@@ -48,6 +48,13 @@ module Gitlab ...@@ -48,6 +48,13 @@ module Gitlab
end end
end end
# Returns true if the key for this lease is set.
def exists?
Gitlab::Redis.with do |redis|
redis.exists(redis_key)
end
end
# No #cancel method. See comments above! # No #cancel method. See comments above!
private private
......
...@@ -21,9 +21,11 @@ describe Projects::Boards::IssuesController do ...@@ -21,9 +21,11 @@ describe Projects::Boards::IssuesController do
context 'with valid list id' do context 'with valid list id' do
it 'returns issues that have the list label applied' 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'))) 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: [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) create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
issue.subscribe(johndoe)
list_issues user: user, board: board, list: list2 list_issues user: user, board: board, list: list2
......
...@@ -116,116 +116,126 @@ describe SnippetsController do ...@@ -116,116 +116,126 @@ describe SnippetsController do
end end
end end
describe 'GET #raw' do %w(raw download).each do |action|
let(:user) { create(:user) } describe "GET #{action}" do
context 'when the personal snippet is private' do
let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
context 'when signed in' do
before do
sign_in(user)
end
context 'when the personal snippet is private' do context 'when signed in user is not the author' do
let(:personal_snippet) { create(:personal_snippet, :private, author: user) } let(:other_author) { create(:author) }
let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
context 'when signed in' do it 'responds with status 404' do
before do get action, id: other_personal_snippet.to_param
sign_in(user)
end
context 'when signed in user is not the author' do expect(response).to have_http_status(404)
let(:other_author) { create(:author) } end
let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) } end
it 'responds with status 404' do context 'when signed in user is the author' do
get :raw, id: other_personal_snippet.to_param before { get action, id: personal_snippet.to_param }
expect(response).to have_http_status(404) it 'responds with status 200' do
end expect(assigns(:snippet)).to eq(personal_snippet)
end expect(response).to have_http_status(200)
end
context 'when signed in user is the author' do it 'has expected headers' do
it 'renders the raw snippet' do expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
get :raw, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet) if action == :download
expect(response).to have_http_status(200) expect(response.header['Content-Disposition']).to match(/attachment/)
elsif action == :raw
expect(response.header['Content-Disposition']).to match(/inline/)
end
end
end end
end end
end
context 'when not signed in' do context 'when not signed in' do
it 'redirects to the sign in page' do it 'redirects to the sign in page' do
get :raw, id: personal_snippet.to_param get action, id: personal_snippet.to_param
expect(response).to redirect_to(new_user_session_path) expect(response).to redirect_to(new_user_session_path)
end
end end
end end
end
context 'when the personal snippet is internal' do context 'when the personal snippet is internal' do
let(:personal_snippet) { create(:personal_snippet, :internal, author: user) } let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
context 'when signed in' do context 'when signed in' do
before do before do
sign_in(user) sign_in(user)
end end
it 'renders the raw snippet' do it 'responds with status 200' do
get :raw, id: personal_snippet.to_param get action, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet) expect(assigns(:snippet)).to eq(personal_snippet)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end
end end
end
context 'when not signed in' do context 'when not signed in' do
it 'redirects to the sign in page' do it 'redirects to the sign in page' do
get :raw, id: personal_snippet.to_param get action, id: personal_snippet.to_param
expect(response).to redirect_to(new_user_session_path) expect(response).to redirect_to(new_user_session_path)
end
end end
end end
end
context 'when the personal snippet is public' do context 'when the personal snippet is public' do
let(:personal_snippet) { create(:personal_snippet, :public, author: user) } let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
context 'when signed in' do context 'when signed in' do
before do before do
sign_in(user) sign_in(user)
end end
it 'renders the raw snippet' do it 'responds with status 200' do
get :raw, id: personal_snippet.to_param get action, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet) expect(assigns(:snippet)).to eq(personal_snippet)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end
end end
end
context 'when not signed in' do context 'when not signed in' do
it 'renders the raw snippet' do it 'responds with status 200' do
get :raw, id: personal_snippet.to_param get action, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet) expect(assigns(:snippet)).to eq(personal_snippet)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end
end end
end end
end
context 'when the personal snippet does not exist' do context 'when the personal snippet does not exist' do
context 'when signed in' do context 'when signed in' do
before do before do
sign_in(user) sign_in(user)
end end
it 'responds with status 404' do it 'responds with status 404' do
get :raw, id: 'doesntexist' get action, id: 'doesntexist'
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end
end end
end
context 'when not signed in' do context 'when not signed in' do
it 'responds with status 404' do it 'responds with status 404' do
get :raw, id: 'doesntexist' get action, id: 'doesntexist'
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end
end end
end end
end end
......
...@@ -66,6 +66,21 @@ describe 'Issue Boards new issue', feature: true, js: true do ...@@ -66,6 +66,21 @@ describe 'Issue Boards new issue', feature: true, js: true do
expect(page).to have_content('1') expect(page).to have_content('1')
end end
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 end
context 'unauthorized user' do context 'unauthorized user' do
......
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
...@@ -58,6 +58,22 @@ feature 'Issue filtering by Milestone', feature: true do ...@@ -58,6 +58,22 @@ feature 'Issue filtering by Milestone', feature: true do
expect(page).to have_css('.issue', count: 1) expect(page).to have_css('.issue', count: 1)
end 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) def visit_issues(project)
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project.namespace, project)
end end
......
...@@ -40,8 +40,6 @@ feature 'Merge request created from fork' do ...@@ -40,8 +40,6 @@ feature 'Merge request created from fork' do
end end
context 'pipeline present in source project' do context 'pipeline present in source project' do
include WaitForAjax
given(:pipeline) do given(:pipeline) do
create(:ci_pipeline, create(:ci_pipeline,
project: fork_project, project: fork_project,
...@@ -57,7 +55,6 @@ feature 'Merge request created from fork' do ...@@ -57,7 +55,6 @@ feature 'Merge request created from fork' do
scenario 'user visits a pipelines page', js: true do scenario 'user visits a pipelines page', js: true do
visit_merge_request(merge_request) visit_merge_request(merge_request)
page.within('.merge-request-tabs') { click_link 'Builds' } page.within('.merge-request-tabs') { click_link 'Builds' }
wait_for_ajax
page.within('table.ci-table') do page.within('table.ci-table') do
expect(page).to have_content 'rspec' expect(page).to have_content 'rspec'
......
...@@ -67,6 +67,23 @@ feature 'Merge Request filtering by Milestone', feature: true do ...@@ -67,6 +67,23 @@ feature 'Merge Request filtering by Milestone', feature: true do
expect(page).to have_css('.merge-request', count: 1) expect(page).to have_css('.merge-request', count: 1)
end 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) def visit_merge_requests(project)
visit namespace_project_merge_requests_path(project.namespace, project) visit namespace_project_merge_requests_path(project.namespace, project)
end end
......
...@@ -77,7 +77,7 @@ feature 'issuable templates', feature: true, js: true do ...@@ -77,7 +77,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "bug" template' do scenario 'user selects "bug" template' do
select_template 'bug' select_template 'bug'
wait_for_ajax wait_for_ajax
preview_template("#{prior_description}\n\n#{template_content}") preview_template("#{template_content}")
save_changes save_changes
end end
end end
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
"iid": { "type": "integer" }, "iid": { "type": "integer" },
"title": { "type": "string" }, "title": { "type": "string" },
"confidential": { "type": "boolean" }, "confidential": { "type": "boolean" },
"due_date": { "type": ["date", "null"] },
"labels": { "labels": {
"type": "array", "type": "array",
"items": { "items": {
...@@ -42,7 +43,8 @@ ...@@ -42,7 +43,8 @@
"name": { "type": "string" }, "name": { "type": "string" },
"username": { "type": "string" }, "username": { "type": "string" },
"avatar_url": { "type": "uri" } "avatar_url": { "type": "uri" }
} },
"subscribed": { "type": ["boolean", "null"] }
}, },
"additionalProperties": false "additionalProperties": false
} }
/*= require jquery.cookie.js */ /*= require js.cookie.js */
/*= require jquery.endless-scroll.js */ /*= require jquery.endless-scroll.js */
/*= require pager */ /*= require pager */
/*= require activities */ /*= require activities */
......
/*= require awards_handler */ /*= require awards_handler */
/*= require jquery */ /*= require jquery */
/*= require jquery.cookie */ /*= require js.cookie */
/*= require ./fixtures/emoji_menu */ /*= require ./fixtures/emoji_menu */
(function() { (function() {
...@@ -44,7 +44,6 @@ ...@@ -44,7 +44,6 @@
spyOn(jQuery, 'get').and.callFake(function(req, cb) { spyOn(jQuery, 'get').and.callFake(function(req, cb) {
return cb(window.emojiMenu); return cb(window.emojiMenu);
}); });
spyOn(jQuery, 'cookie');
}); });
afterEach(function() { afterEach(function() {
// restore original url root value // restore original url root value
...@@ -190,28 +189,6 @@ ...@@ -190,28 +189,6 @@
return expect($thumbsUpEmoji.data("original-title")).toBe('sam'); return expect($thumbsUpEmoji.data("original-title")).toBe('sam');
}); });
}); });
describe('::addEmojiToFrequentlyUsedList', function() {
it('should set a cookie with the correct default path', function() {
gon.relative_url_root = '';
awardsHandler.addEmojiToFrequentlyUsedList('sunglasses');
expect(jQuery.cookie)
.toHaveBeenCalledWith('frequently_used_emojis', 'sunglasses', {
path: '/',
expires: 365
})
;
});
it('should set a cookie with the correct custom root path', function() {
gon.relative_url_root = '/gitlab/subdir';
awardsHandler.addEmojiToFrequentlyUsedList('alien');
expect(jQuery.cookie)
.toHaveBeenCalledWith('frequently_used_emojis', 'alien', {
path: '/gitlab/subdir',
expires: 365
})
;
});
});
describe('search', function() { describe('search', function() {
return it('should filter the emoji', function() { return it('should filter the emoji', function() {
$('.js-add-award').eq(0).click(); $('.js-add-award').eq(0).click();
......
//= require jquery //= require jquery
//= require jquery_ujs //= require jquery_ujs
//= require jquery.cookie //= require js.cookie
//= require vue //= require vue
//= require vue-resource //= require vue-resource
//= require lib/utils/url_utility //= require lib/utils/url_utility
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
gl.boardService = new BoardService('/test/issue-boards/board', '1'); gl.boardService = new BoardService('/test/issue-boards/board', '1');
gl.issueBoards.BoardsStore.create(); gl.issueBoards.BoardsStore.create();
$.cookie('issue_board_welcome_hidden', 'false'); Cookies.set('issue_board_welcome_hidden', 'false');
}); });
describe('Store', () => { describe('Store', () => {
......
//= require jquery //= require jquery
//= require jquery_ujs //= require jquery_ujs
//= require jquery.cookie //= require js.cookie
//= require vue //= require vue
//= require vue-resource //= require vue-resource
//= require lib/utils/url_utility //= require lib/utils/url_utility
......
//= require jquery //= require jquery
//= require jquery_ujs //= require jquery_ujs
//= require jquery.cookie //= require js.cookie
//= require vue //= require vue
//= require vue-resource //= require vue-resource
//= require lib/utils/url_utility //= require lib/utils/url_utility
......
/*= require right_sidebar */ /*= require right_sidebar */
/*= require jquery */ /*= require jquery */
/*= require jquery.cookie */ /*= require js.cookie */
(function() { (function() {
var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState; var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState;
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::ExclusiveLease do describe Gitlab::ExclusiveLease, type: :redis do
it 'cannot obtain twice before the lease has expired' do let(:unique_key) { SecureRandom.hex(10) }
lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600)
expect(lease.try_obtain).to eq(true) describe '#try_obtain' do
expect(lease.try_obtain).to eq(false) it 'cannot obtain twice before the lease has expired' do
end lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600)
expect(lease.try_obtain).to eq(true)
expect(lease.try_obtain).to eq(false)
end
it 'can obtain after the lease has expired' do it 'can obtain after the lease has expired' do
timeout = 1 timeout = 1
lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout) lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout)
lease.try_obtain # start the lease lease.try_obtain # start the lease
sleep(2 * timeout) # lease should have expired now sleep(2 * timeout) # lease should have expired now
expect(lease.try_obtain).to eq(true) expect(lease.try_obtain).to eq(true)
end
end end
def unique_key describe '#exists?' do
SecureRandom.hex(10) it 'returns true for an existing lease' do
lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600)
lease.try_obtain
expect(lease.exists?).to eq(true)
end
it 'returns false for a lease that does not exist' do
lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600)
expect(lease.exists?).to eq(false)
end
end end
end end
...@@ -6,6 +6,7 @@ describe API::API, api: true do ...@@ -6,6 +6,7 @@ describe API::API, api: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:project) { create(:project, creator_id: user.id) } 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_key) { create(:deploy_key, public: true) }
let!(:deploy_keys_project) do let!(:deploy_keys_project) do
...@@ -96,6 +97,22 @@ describe API::API, api: true do ...@@ -96,6 +97,22 @@ describe API::API, api: true do
post api("/projects/#{project.id}/deploy_keys", admin), key_attrs post api("/projects/#{project.id}/deploy_keys", admin), key_attrs
end.to change{ project.deploy_keys.count }.by(1) end.to change{ project.deploy_keys.count }.by(1)
end 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 end
describe 'DELETE /projects/:id/deploy_keys/:key_id' do describe 'DELETE /projects/:id/deploy_keys/:key_id' do
......
...@@ -958,6 +958,29 @@ describe API::API, api: true do ...@@ -958,6 +958,29 @@ describe API::API, api: true do
expect(joined_event['author']['name']).to eq(user.name) expect(joined_event['author']['name']).to eq(user.name)
end end
end end
context 'when there are multiple events from different projects' do
let(:second_note) { create(:note_on_issue, project: create(:empty_project)) }
let(:third_note) { create(:note_on_issue, project: project) }
before do
second_note.project.add_user(user, :developer)
[second_note, third_note].each do |note|
EventCreateService.new.leave_note(note, user)
end
end
it 'returns events in the correct order (from newest to oldest)' do
get api("/users/#{user.id}/events", user)
comment_events = json_response.select { |e| e['action_name'] == 'commented on' }
expect(comment_events[0]['target_id']).to eq(third_note.id)
expect(comment_events[1]['target_id']).to eq(second_note.id)
expect(comment_events[2]['target_id']).to eq(note.id)
end
end
end end
it 'returns a 404 error if not found' do it 'returns a 404 error if not found' do
......
...@@ -50,6 +50,12 @@ RSpec.configure do |config| ...@@ -50,6 +50,12 @@ RSpec.configure do |config|
example.run example.run
Rails.cache = caching_store Rails.cache = caching_store
end end
config.around(:each, :redis) do |example|
Gitlab::Redis.with(&:flushall)
example.run
Gitlab::Redis.with(&:flushall)
end
end end
FactoryGirl::SyntaxRunner.class_eval do FactoryGirl::SyntaxRunner.class_eval do
......
...@@ -5,6 +5,26 @@ describe ProjectCacheWorker do ...@@ -5,6 +5,26 @@ describe ProjectCacheWorker do
subject { described_class.new } subject { described_class.new }
describe '.perform_async' do
it 'schedules the job when no lease exists' do
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?).
and_return(false)
expect_any_instance_of(described_class).to receive(:perform)
described_class.perform_async(project.id)
end
it 'does not schedule the job when a lease exists' do
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?).
and_return(true)
expect_any_instance_of(described_class).not_to receive(:perform)
described_class.perform_async(project.id)
end
end
describe '#perform' do describe '#perform' do
context 'when an exclusive lease can be obtained' do context 'when an exclusive lease can be obtained' do
before do before do
......
/**
* jQuery Cookie plugin
*
* Copyright (c) 2010 Klaus Hartl (stilbuero.de)
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
*/
jQuery.cookie = function (key, value, options) {
// key and at least value given, set cookie...
if (arguments.length > 1 && String(value) !== "[object Object]") {
options = jQuery.extend({}, options);
if (value === null || value === undefined) {
options.expires = -1;
}
if (typeof options.expires === 'number') {
var days = options.expires, t = options.expires = new Date();
t.setDate(t.getDate() + days);
}
value = String(value);
return (document.cookie = [
encodeURIComponent(key), '=',
options.raw ? value : encodeURIComponent(value),
options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
options.path ? '; path=' + options.path : '',
options.domain ? '; domain=' + options.domain : '',
options.secure ? '; secure' : ''
].join(''));
}
// key and possibly options given, get cookie...
options = value || {};
var result, decode = options.raw ? function (s) { return s; } : decodeURIComponent;
return (result = new RegExp('(?:^|; )' + encodeURIComponent(key) + '=([^;]*)').exec(document.cookie)) ? decode(result[1]) : null;
};
/*!
* JavaScript Cookie v2.1.3
* https://github.com/js-cookie/js-cookie
*
* Copyright 2006, 2015 Klaus Hartl & Fagner Brack
* Released under the MIT license
*/
;(function (factory) {
var registeredInModuleLoader = false;
if (typeof define === 'function' && define.amd) {
define(factory);
registeredInModuleLoader = true;
}
if (typeof exports === 'object') {
module.exports = factory();
registeredInModuleLoader = true;
}
if (!registeredInModuleLoader) {
var OldCookies = window.Cookies;
var api = window.Cookies = factory();
api.noConflict = function () {
window.Cookies = OldCookies;
return api;
};
}
}(function () {
function extend () {
var i = 0;
var result = {};
for (; i < arguments.length; i++) {
var attributes = arguments[ i ];
for (var key in attributes) {
result[key] = attributes[key];
}
}
return result;
}
function init (converter) {
function api (key, value, attributes) {
var result;
if (typeof document === 'undefined') {
return;
}
// Write
if (arguments.length > 1) {
attributes = extend({
path: '/'
}, api.defaults, attributes);
if (typeof attributes.expires === 'number') {
var expires = new Date();
expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5);
attributes.expires = expires;
}
try {
result = JSON.stringify(value);
if (/^[\{\[]/.test(result)) {
value = result;
}
} catch (e) {}
if (!converter.write) {
value = encodeURIComponent(String(value))
.replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent);
} else {
value = converter.write(value, key);
}
key = encodeURIComponent(String(key));
key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent);
key = key.replace(/[\(\)]/g, escape);
return (document.cookie = [
key, '=', value,
attributes.expires ? '; expires=' + attributes.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
attributes.path ? '; path=' + attributes.path : '',
attributes.domain ? '; domain=' + attributes.domain : '',
attributes.secure ? '; secure' : ''
].join(''));
}
// Read
if (!key) {
result = {};
}
// To prevent the for loop in the first place assign an empty array
// in case there are no cookies at all. Also prevents odd result when
// calling "get()"
var cookies = document.cookie ? document.cookie.split('; ') : [];
var rdecode = /(%[0-9A-Z]{2})+/g;
var i = 0;
for (; i < cookies.length; i++) {
var parts = cookies[i].split('=');
var cookie = parts.slice(1).join('=');
if (cookie.charAt(0) === '"') {
cookie = cookie.slice(1, -1);
}
try {
var name = parts[0].replace(rdecode, decodeURIComponent);
cookie = converter.read ?
converter.read(cookie, name) : converter(cookie, name) ||
cookie.replace(rdecode, decodeURIComponent);
if (this.json) {
try {
cookie = JSON.parse(cookie);
} catch (e) {}
}
if (key === name) {
result = cookie;
break;
}
if (!key) {
result[name] = cookie;
}
} catch (e) {}
}
return result;
}
api.set = api;
api.get = function (key) {
return api.call(api, key);
};
api.getJSON = function () {
return api.apply({
json: true
}, [].slice.call(arguments));
};
api.defaults = {};
api.remove = function (key, attributes) {
api(key, '', extend(attributes, {
expires: -1
}));
};
api.withConverter = init;
return api;
}
return init(function () {});
}));
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment