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 ...
This commit is contained in:
commit
7cdb238ac5
|
@ -279,16 +279,20 @@ bundler:audit:
|
|||
migration paths:
|
||||
stage: test
|
||||
<<: *use-db
|
||||
variables:
|
||||
SETUP_DB: "false"
|
||||
only:
|
||||
- master@gitlab-org/gitlab-ce
|
||||
script:
|
||||
- git checkout HEAD .
|
||||
- git fetch --tags
|
||||
- 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
|
||||
- rake db:drop db:create db:schema:load db:seed_fu
|
||||
- git checkout $CI_BUILD_REF
|
||||
- source scripts/prepare_build.sh
|
||||
- rake db:migrate
|
||||
|
||||
coverage:
|
||||
|
|
45
CHANGELOG.md
45
CHANGELOG.md
|
@ -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 HipChat notifications rendering (airatshigapov, eisnerd)
|
||||
- Add hover to trash icon in notes !7008 (blackst0ne)
|
||||
- Fix sidekiq stats in admin area (blackst0ne)
|
||||
- Escape ref and path for relative links !6050 (winniehell)
|
||||
- Fixed link typo on /help/ui to Alerts section. !6915 (Sam Rose)
|
||||
- Fix filtering of milestones with quotes in title (airatshigapov)
|
||||
- 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 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
|
||||
- Fix Sign in page 'Forgot your password?' link overlaps on medium-large screens
|
||||
- Fix documents and comments on Build API `scope`
|
||||
- Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov)
|
||||
|
||||
## 8.13.1 (unreleased)
|
||||
- Fix bug where labels would be assigned to issues that were moved
|
||||
- Fix error in generating labels
|
||||
- Fix reply-by-email not working due to queue name mismatch
|
||||
- Fixed hidden pipeline graph on commit and MR page !6895
|
||||
- Expire and build repository cache after project import
|
||||
- Fix 404 for group pages when GitLab setup uses relative url
|
||||
- Simpler arguments passed to named_route on toggle_award_url helper method
|
||||
- Fix unauthorized users dragging on issue boards
|
||||
- Better handle when no users were selected for adding to group or project. (Linus Thiel)
|
||||
- Only show register tab if signup enabled.
|
||||
## 8.13.1 (2016-10-25)
|
||||
- Fix branch protection API. !6215
|
||||
- Fix hidden pipeline graph on commit and MR page. !6895
|
||||
- Fix Cycle analytics not showing correct data when filtering by date. !6906
|
||||
- Ensure custom provider tab labels don't break layout. !6993
|
||||
- Fix issue boards user link when in subdirectory. !7018
|
||||
- Refactor and add new environment functionality to CI yaml reference. !7026
|
||||
- Fix typo in project settings that prevents users from enabling container registry. !7037
|
||||
- Fix events order in `users/:id/events` endpoint. !7039
|
||||
- Remove extra line for empty issue description. !7045
|
||||
- Don't append issue/MR templates to any existing text. !7050
|
||||
- Fix error in generating labels. !7055
|
||||
- Stop clearing the database cache on `rake cache:clear`. !7056
|
||||
- Only show register tab if signup enabled. !7058
|
||||
- Expire and build repository cache after project import. !7064
|
||||
- Fix bug where labels would be assigned to issues that were moved. !7065
|
||||
- Fix reply-by-email not working due to queue name mismatch. !7068
|
||||
- Fix 404 for group pages when GitLab setup uses relative url. !7071
|
||||
- Fix `User#to_reference`. !7088
|
||||
- Reduce overhead of `LabelFinder` by avoiding `#presence` call. !7094
|
||||
- Fix unauthorized users dragging on issue boards. !7096
|
||||
- Only schedule `ProjectCacheWorker` jobs when needed. !7099
|
||||
|
||||
## 8.13.0 (2016-10-22)
|
||||
- Removes extra line for empty issue description. (!7045)
|
||||
- Fix save button on project pipeline settings page. (!6955)
|
||||
- All Sidekiq workers now use their own queue
|
||||
- Avoid race condition when asynchronously removing expired artifacts. (!6881)
|
||||
|
@ -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
|
||||
- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
|
||||
- Add group level labels. (!6425)
|
||||
- Fix Cycle analytics not showing correct data when filtering by date. !6906
|
||||
- Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun)
|
||||
- Cancelled pipelines could be retried. !6927
|
||||
- Updating verbiage on git basics to be more intuitive
|
||||
|
@ -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)
|
||||
- The instrumentation for Banzai::Renderer has been restored
|
||||
- Change user & group landing page routing from /u/:username to /:username
|
||||
- Fixed issue boards user link when in subdirectory
|
||||
- Added documentation for .gitattributes files
|
||||
- Move Pipeline Metrics to separate worker
|
||||
- AbstractReferenceFilter caches project_refs on RequestStore when active
|
||||
|
@ -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)
|
||||
- Add tag shortcut from the Commit page. !6543
|
||||
- Keep refs for each deployment
|
||||
- Close open tooltips on page navigation (Linus Thiel)
|
||||
- Allow browsing branches that end with '.atom'
|
||||
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
|
||||
- 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.
|
|||
- Add RTL support to markdown renderer (Ebrahim Byagowi)
|
||||
- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
|
||||
- Fix todos page mobile viewport layout (ClemMakesApps)
|
||||
- Make issues search less finicky
|
||||
- Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps)
|
||||
- Remove redundant mixins (ClemMakesApps)
|
||||
- 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.
|
|||
- Fix inconsistent checkbox alignment (ClemMakesApps)
|
||||
- Use the default branch for displaying the project icon instead of master !5792 (Hannes Rosenögger)
|
||||
- Adds response mime type to transaction metric action when it's not HTML
|
||||
- Fix branch protection API !6215
|
||||
- Fix hover leading space bug in pipeline graph !5980
|
||||
- Avoid conflict with admin labels when importing GitHub labels
|
||||
- User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496
|
||||
|
|
|
@ -24,9 +24,7 @@
|
|||
var filter = sender.attr("id").split("_")[0];
|
||||
|
||||
$('.event-filter .active').removeClass("active");
|
||||
$.cookie("event_filter", filter, {
|
||||
path: gon.relative_url_root || '/'
|
||||
});
|
||||
Cookies.set("event_filter", filter);
|
||||
|
||||
sender.closest('li').toggleClass("active");
|
||||
};
|
||||
|
|
|
@ -11,13 +11,13 @@
|
|||
/*= require jquery-ui/effect-highlight */
|
||||
/*= require jquery-ui/sortable */
|
||||
/*= require jquery_ujs */
|
||||
/*= require jquery.cookie */
|
||||
/*= require jquery.endless-scroll */
|
||||
/*= require jquery.highlight */
|
||||
/*= require jquery.waitforimages */
|
||||
/*= require jquery.atwho */
|
||||
/*= require jquery.scrollTo */
|
||||
/*= require jquery.turbolinks */
|
||||
/*= require js.cookie */
|
||||
/*= require turbolinks */
|
||||
/*= require autosave */
|
||||
/*= require bootstrap/affix */
|
||||
|
@ -124,15 +124,11 @@
|
|||
return str.replace(/<(?:.|\n)*?>/gm, '');
|
||||
};
|
||||
|
||||
window.unbindEvents = function() {
|
||||
return $(document).off('scroll');
|
||||
};
|
||||
|
||||
window.shiftWindow = function() {
|
||||
return scrollBy(0, -100);
|
||||
};
|
||||
|
||||
document.addEventListener("page:fetch", unbindEvents);
|
||||
document.addEventListener("page:fetch", gl.utils.cleanupBeforeFetch);
|
||||
|
||||
window.addEventListener("hashchange", shiftWindow);
|
||||
|
||||
|
@ -149,6 +145,10 @@
|
|||
$document = $(document);
|
||||
$window = $(window);
|
||||
$body = $('body');
|
||||
|
||||
// Set the default path for all cookies to GitLab's root directory
|
||||
Cookies.defaults.path = gon.relative_url_root || '/';
|
||||
|
||||
gl.utils.preventDisabledButtons();
|
||||
bootstrapBreakpoint = bp.getBreakpointSize();
|
||||
$(".nav-sidebar").niceScroll({
|
||||
|
|
|
@ -322,21 +322,18 @@
|
|||
var frequentlyUsedEmojis;
|
||||
frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
|
||||
frequentlyUsedEmojis.push(emoji);
|
||||
return $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), {
|
||||
path: gon.relative_url_root || '/',
|
||||
expires: 365
|
||||
});
|
||||
Cookies.set('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 });
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.getFrequentlyUsedEmojis = function() {
|
||||
var frequentlyUsedEmojis;
|
||||
frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(',');
|
||||
frequentlyUsedEmojis = (Cookies.get('frequently_used_emojis') || '').split(',');
|
||||
return _.compact(_.uniq(frequentlyUsedEmojis));
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.renderFrequentlyUsedBlock = function() {
|
||||
var emoji, frequentlyUsedEmojis, i, len, ul;
|
||||
if ($.cookie('frequently_used_emojis')) {
|
||||
if (Cookies.get('frequently_used_emojis')) {
|
||||
frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
|
||||
ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>");
|
||||
for (i = 0, len = frequentlyUsedEmojis.length; i < len; i++) {
|
||||
|
|
|
@ -68,14 +68,10 @@
|
|||
// To be implemented on the extending class
|
||||
// e.g.
|
||||
// Api.gitignoreText item.name, @requestFileSuccess.bind(@)
|
||||
requestFileSuccess(file, { skipFocus, append } = {}) {
|
||||
requestFileSuccess(file, { skipFocus } = {}) {
|
||||
const oldValue = this.editor.getValue();
|
||||
let newValue = file.content;
|
||||
|
||||
if (append && oldValue.length && oldValue !== newValue) {
|
||||
newValue = oldValue + '\n\n' + newValue;
|
||||
}
|
||||
|
||||
this.editor.setValue(newValue, 1);
|
||||
if (!skipFocus) this.editor.focus();
|
||||
|
||||
|
@ -99,4 +95,3 @@
|
|||
|
||||
global.TemplateSelector = TemplateSelector;
|
||||
})(window.gl || ( window.gl = {}));
|
||||
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
//= require_tree ./stores
|
||||
//= require_tree ./services
|
||||
//= require_tree ./mixins
|
||||
//= require_tree ./filters
|
||||
//= require ./components/board
|
||||
//= require ./components/board_sidebar
|
||||
//= require ./components/new_list_dropdown
|
||||
//= require ./vue_resource_interceptor
|
||||
|
||||
|
@ -22,7 +24,8 @@ $(() => {
|
|||
gl.IssueBoardsApp = new Vue({
|
||||
el: $boardApp,
|
||||
components: {
|
||||
'board': gl.issueBoards.Board
|
||||
'board': gl.issueBoards.Board,
|
||||
'board-sidebar': gl.issueBoards.BoardSidebar
|
||||
},
|
||||
data: {
|
||||
state: Store.state,
|
||||
|
@ -30,9 +33,15 @@ $(() => {
|
|||
endpoint: $boardApp.dataset.endpoint,
|
||||
boardId: $boardApp.dataset.boardId,
|
||||
disabled: $boardApp.dataset.disabled === 'true',
|
||||
issueLinkBase: $boardApp.dataset.issueLinkBase
|
||||
issueLinkBase: $boardApp.dataset.issueLinkBase,
|
||||
detailIssue: Store.detail
|
||||
},
|
||||
init: Store.create.bind(Store),
|
||||
computed: {
|
||||
detailIssueVisible () {
|
||||
return Object.keys(this.detailIssue.issue).length;
|
||||
}
|
||||
},
|
||||
created () {
|
||||
gl.boardService = new BoardService(this.endpoint, this.boardId);
|
||||
},
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
},
|
||||
data () {
|
||||
return {
|
||||
detailIssue: Store.detail,
|
||||
filters: Store.state.filters,
|
||||
showIssueForm: false
|
||||
};
|
||||
|
@ -32,6 +33,26 @@
|
|||
this.list.getIssues(true);
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
detailIssue: {
|
||||
handler () {
|
||||
if (!Object.keys(this.detailIssue.issue).length) return;
|
||||
|
||||
const issue = this.list.findIssue(this.detailIssue.issue.id);
|
||||
|
||||
if (issue) {
|
||||
const boardsList = document.querySelectorAll('.boards-list')[0];
|
||||
const right = (this.$el.offsetLeft + this.$el.offsetWidth) - boardsList.offsetWidth;
|
||||
const left = boardsList.scrollLeft - this.$el.offsetLeft;
|
||||
|
||||
if (right - boardsList.scrollLeft > 0) {
|
||||
boardsList.scrollLeft = right;
|
||||
} else if (left > 0) {
|
||||
boardsList.scrollLeft = this.$el.offsetLeft;
|
||||
}
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -12,6 +12,17 @@
|
|||
disabled: Boolean,
|
||||
index: Number
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showDetail: false,
|
||||
detailIssue: Store.detail
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
issueDetailVisible () {
|
||||
return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
filterByLabel (label, e) {
|
||||
let labelToggleText = label.title;
|
||||
|
@ -37,6 +48,29 @@
|
|||
$('.labels-filter .dropdown-toggle-text').text(labelToggleText);
|
||||
|
||||
Store.updateFiltersUrl();
|
||||
},
|
||||
mouseDown () {
|
||||
this.showDetail = true;
|
||||
},
|
||||
mouseMove () {
|
||||
if (this.showDetail) {
|
||||
this.showDetail = false;
|
||||
}
|
||||
},
|
||||
showIssue (e) {
|
||||
const targetTagName = e.target.tagName.toLowerCase();
|
||||
|
||||
if (targetTagName === 'a' || targetTagName === 'button') return;
|
||||
|
||||
if (this.showDetail) {
|
||||
this.showDetail = false;
|
||||
|
||||
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
|
||||
Store.detail.issue = {};
|
||||
} else {
|
||||
Store.detail.issue = this.issue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
|
||||
gl.issueBoards.BoardNewIssue = Vue.extend({
|
||||
|
@ -27,13 +29,16 @@
|
|||
const labels = this.list.label ? [this.list.label] : [];
|
||||
const issue = new ListIssue({
|
||||
title: this.title,
|
||||
labels
|
||||
labels,
|
||||
subscribed: true
|
||||
});
|
||||
|
||||
this.list.newIssue(issue)
|
||||
.then((data) => {
|
||||
// Need this because our jQuery very kindly disables buttons on ALL form submissions
|
||||
$(this.$els.submitButton).enable();
|
||||
|
||||
Store.detail.issue = issue;
|
||||
})
|
||||
.catch(() => {
|
||||
// Need this because our jQuery very kindly disables buttons on ALL form submissions
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.BoardSidebar = Vue.extend({
|
||||
props: {
|
||||
currentUser: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
detail: Store.detail,
|
||||
issue: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showSidebar () {
|
||||
return Object.keys(this.issue).length;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
detail: {
|
||||
handler () {
|
||||
this.issue = this.detail.issue;
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
issue () {
|
||||
if (this.showSidebar) {
|
||||
this.$nextTick(() => {
|
||||
$('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0);
|
||||
$('.right-sidebar').getNiceScroll().resize();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeSidebar () {
|
||||
this.detail.issue = {};
|
||||
}
|
||||
},
|
||||
ready () {
|
||||
new IssuableContext(this.currentUser);
|
||||
new MilestoneSelect();
|
||||
new gl.DueDateSelectors();
|
||||
new LabelsSelect();
|
||||
new Sidebar();
|
||||
new Subscription('.subscription');
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,4 @@
|
|||
Vue.filter('due-date', (value) => {
|
||||
const date = new Date(value);
|
||||
return $.datepicker.formatDate('M d, yy', date);
|
||||
});
|
|
@ -22,7 +22,7 @@
|
|||
fallbackOnBody: true,
|
||||
ghostClass: 'is-ghost',
|
||||
filter: '.has-tooltip, .btn',
|
||||
delay: gl.issueBoards.touchEnabled ? 100 : 0,
|
||||
delay: gl.issueBoards.touchEnabled ? 100 : 50,
|
||||
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
|
||||
scrollSpeed: 20,
|
||||
onStart: gl.issueBoards.onStart,
|
||||
|
|
|
@ -3,12 +3,18 @@ class ListIssue {
|
|||
this.id = obj.iid;
|
||||
this.title = obj.title;
|
||||
this.confidential = obj.confidential;
|
||||
this.dueDate = obj.due_date;
|
||||
this.subscribed = obj.subscribed;
|
||||
this.labels = [];
|
||||
|
||||
if (obj.assignee) {
|
||||
this.assignee = new ListUser(obj.assignee);
|
||||
}
|
||||
|
||||
if (obj.milestone) {
|
||||
this.milestone = new ListMilestone(obj.milestone);
|
||||
}
|
||||
|
||||
obj.labels.forEach((label) => {
|
||||
this.labels.push(new ListLabel(label));
|
||||
});
|
||||
|
@ -41,4 +47,21 @@ class ListIssue {
|
|||
getLists () {
|
||||
return gl.issueBoards.BoardsStore.state.lists.filter( list => list.findIssue(this.id) );
|
||||
}
|
||||
|
||||
update (url) {
|
||||
const data = {
|
||||
issue: {
|
||||
milestone_id: this.milestone ? this.milestone.id : null,
|
||||
due_date: this.dueDate,
|
||||
assignee_id: this.assignee ? this.assignee.id : null,
|
||||
label_ids: this.labels.map( (label) => label.id )
|
||||
}
|
||||
};
|
||||
|
||||
if (!data.issue.label_ids.length) {
|
||||
data.issue.label_ids = [''];
|
||||
}
|
||||
|
||||
return Vue.http.patch(url, data);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
class ListMilestone {
|
||||
constructor (obj) {
|
||||
this.id = obj.id;
|
||||
this.title = obj.title;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
class BoardService {
|
||||
constructor (root, boardId) {
|
||||
Vue.http.options.root = root;
|
||||
|
||||
this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
|
||||
generate: {
|
||||
method: 'POST',
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
gl.issueBoards.BoardsStore = {
|
||||
disabled: false,
|
||||
state: {},
|
||||
detail: {
|
||||
issue: {}
|
||||
},
|
||||
moving: {
|
||||
issue: {},
|
||||
list: {}
|
||||
|
@ -58,12 +61,12 @@
|
|||
removeBlankState () {
|
||||
this.removeList('blank');
|
||||
|
||||
$.cookie('issue_board_welcome_hidden', 'true', {
|
||||
Cookies.set('issue_board_welcome_hidden', 'true', {
|
||||
expires: 365 * 10
|
||||
});
|
||||
},
|
||||
welcomeIsHidden () {
|
||||
return $.cookie('issue_board_welcome_hidden') === 'true';
|
||||
return Cookies.get('issue_board_welcome_hidden') === 'true';
|
||||
},
|
||||
removeList (id, type = 'blank') {
|
||||
const list = this.findList('id', id, type);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
const store = gl.cycleAnalyticsStore = {
|
||||
isLoading: true,
|
||||
hasError: false,
|
||||
isHelpDismissed: $.cookie(COOKIE_NAME),
|
||||
isHelpDismissed: Cookies.get(COOKIE_NAME),
|
||||
analytics: {}
|
||||
};
|
||||
|
||||
|
@ -75,9 +75,7 @@
|
|||
|
||||
dismissLanding() {
|
||||
store.isHelpDismissed = true;
|
||||
$.cookie(COOKIE_NAME, true, {
|
||||
path: gon.relative_url_root || '/'
|
||||
});
|
||||
Cookies.set(COOKIE_NAME, true);
|
||||
}
|
||||
|
||||
initDropdown() {
|
||||
|
|
|
@ -41,7 +41,12 @@
|
|||
defaultDate: $("input[name='" + this.fieldName + "']").val(),
|
||||
altField: "input[name='" + this.fieldName + "']",
|
||||
onSelect: () => {
|
||||
return this.saveDueDate(true);
|
||||
if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
|
||||
gl.issueBoards.BoardsStore.detail.issue.dueDate = $(`input[name='${this.fieldName}']`).val();
|
||||
this.updateIssueBoardIssue();
|
||||
} else {
|
||||
return this.saveDueDate(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -49,8 +54,14 @@
|
|||
initRemoveDueDate() {
|
||||
this.$block.on('click', '.js-remove-due-date', (e) => {
|
||||
e.preventDefault();
|
||||
$("input[name='" + this.fieldName + "']").val('');
|
||||
return this.saveDueDate(false);
|
||||
|
||||
if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
|
||||
gl.issueBoards.BoardsStore.detail.issue.dueDate = '';
|
||||
this.updateIssueBoardIssue();
|
||||
} else {
|
||||
$("input[name='" + this.fieldName + "']").val('');
|
||||
return this.saveDueDate(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -83,6 +94,18 @@
|
|||
this.datePayload = datePayload;
|
||||
}
|
||||
|
||||
updateIssueBoardIssue () {
|
||||
this.$loading.fadeIn();
|
||||
this.$dropdown.trigger('loading.gl.dropdown');
|
||||
this.$selectbox.hide();
|
||||
this.$value.css('display', '');
|
||||
|
||||
gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update'))
|
||||
.then(() => {
|
||||
this.$loading.fadeOut();
|
||||
});
|
||||
}
|
||||
|
||||
submitSelectedDate(isDropdown) {
|
||||
return $.ajax({
|
||||
type: 'PUT',
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
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 @@
|
|||
value = this.options.id ? this.options.id(data) : data.id;
|
||||
fieldName = this.options.fieldName;
|
||||
|
||||
if (value) { value = value.toString().replace(/'/g, '\\\'') };
|
||||
|
||||
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
|
||||
if (field.length) {
|
||||
selected = true;
|
||||
|
@ -620,6 +622,17 @@
|
|||
selectedObject = this.renderedData[selectedIndex];
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.vue) {
|
||||
if (el.hasClass(ACTIVE_CLASS)) {
|
||||
el.removeClass(ACTIVE_CLASS);
|
||||
} else {
|
||||
el.addClass(ACTIVE_CLASS);
|
||||
}
|
||||
|
||||
return selectedObject;
|
||||
}
|
||||
|
||||
field = [];
|
||||
value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
|
||||
if (isInput) {
|
||||
|
|
|
@ -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> <% }); %>');
|
||||
},
|
||||
initSearch: function() {
|
||||
// `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
|
||||
const debouncedExecSearch = _.debounce(Issuable.executeSearch, 500, false);
|
||||
const $searchInput = $('#issuable_search');
|
||||
|
||||
$('#issuable_search').off('keyup').on('keyup', debouncedExecSearch);
|
||||
Issuable.initSearchState($searchInput);
|
||||
|
||||
// `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
|
||||
const debouncedExecSearch = _.debounce(Issuable.executeSearch, 1000, false);
|
||||
|
||||
$searchInput.off('keyup').on('keyup', debouncedExecSearch);
|
||||
|
||||
// ensures existing filters are preserved when manually submitted
|
||||
$('#issue_search_form').on('submit', (e) => {
|
||||
$('#issuable_search_form').on('submit', (e) => {
|
||||
e.preventDefault();
|
||||
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) {
|
||||
const $search = $('#issuable_search');
|
||||
|
@ -32,6 +77,11 @@
|
|||
const $searchValue = $search.val();
|
||||
const $filtersForm = $('.js-filter-form');
|
||||
const $input = $(`input[name='${$searchName}']`, $filtersForm);
|
||||
const isPristine = Issuable.accessSearchPristine();
|
||||
|
||||
if (isPristine) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$input.length) {
|
||||
$filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`);
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
abilityName = $dropdown.data('ability-name');
|
||||
$selectbox = $dropdown.closest('.selectbox');
|
||||
$block = $selectbox.closest('.block');
|
||||
$form = $dropdown.closest('form');
|
||||
$form = $dropdown.closest('form, .js-issuable-update');
|
||||
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
|
||||
$sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
|
||||
$value = $block.find('.value');
|
||||
|
@ -317,6 +317,7 @@
|
|||
}
|
||||
},
|
||||
multiSelect: $dropdown.hasClass('js-multiselect'),
|
||||
vue: $dropdown.hasClass('js-issue-board-sidebar'),
|
||||
clicked: function(label, $el, e) {
|
||||
var isIssueIndex, isMRIndex, page;
|
||||
_this.enableBulkLabelDropdown();
|
||||
|
@ -334,7 +335,7 @@
|
|||
page = $('body').data('page');
|
||||
isIssueIndex = page === 'projects:issues:index';
|
||||
isMRIndex = page === 'projects:merge_requests:index';
|
||||
if ($('html').hasClass('issue-boards-page')) {
|
||||
if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
|
||||
if (label.isAny) {
|
||||
gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
|
||||
}
|
||||
|
@ -362,6 +363,30 @@
|
|||
else if ($dropdown.hasClass('js-filter-submit')) {
|
||||
return $dropdown.closest('form').submit();
|
||||
}
|
||||
else if ($dropdown.hasClass('js-issue-board-sidebar')) {
|
||||
if ($el.hasClass('is-active')) {
|
||||
gl.issueBoards.BoardsStore.detail.issue.labels.push(new ListLabel({
|
||||
id: label.id,
|
||||
title: label.title,
|
||||
color: label.color[0],
|
||||
textColor: '#fff'
|
||||
}));
|
||||
}
|
||||
else {
|
||||
var labels = gl.issueBoards.BoardsStore.detail.issue.labels;
|
||||
labels = labels.filter(function (selectedLabel) {
|
||||
return selectedLabel.id !== label.id;
|
||||
});
|
||||
gl.issueBoards.BoardsStore.detail.issue.labels = labels;
|
||||
}
|
||||
|
||||
$loading.fadeIn();
|
||||
|
||||
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
|
||||
.then(function () {
|
||||
$loading.fadeOut();
|
||||
});
|
||||
}
|
||||
else {
|
||||
if ($dropdown.hasClass('js-multiselect')) {
|
||||
|
||||
|
|
|
@ -43,6 +43,14 @@
|
|||
parser.href = url;
|
||||
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) {
|
||||
var suffixFromNow, timefor;
|
||||
if (!time) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
((global) => {
|
||||
global.mergeConflicts = global.mergeConflicts || {};
|
||||
|
||||
const diffViewType = $.cookie('diff_view');
|
||||
const diffViewType = Cookies.get('diff_view');
|
||||
const HEAD_HEADER_TEXT = 'HEAD//our changes';
|
||||
const ORIGIN_HEADER_TEXT = 'origin//their changes';
|
||||
const HEAD_BUTTON_TITLE = 'Use ours';
|
||||
|
@ -180,9 +180,7 @@
|
|||
this.state.diffView = viewType;
|
||||
this.state.isParallel = viewType === VIEW_TYPES.PARALLEL;
|
||||
|
||||
$.cookie('diff_view', viewType, {
|
||||
path: gon.relative_url_root || '/'
|
||||
});
|
||||
Cookies.set('diff_view', viewType);
|
||||
},
|
||||
|
||||
getHeadHeaderLine(id) {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
// Handles persisting and restoring the current tab selection and lazily-loading
|
||||
// content on the MergeRequests#show page.
|
||||
//
|
||||
/*= require jquery.cookie */
|
||||
/*= require js.cookie */
|
||||
|
||||
//
|
||||
// ### Example Markup
|
||||
|
@ -368,7 +368,7 @@
|
|||
|
||||
MergeRequestTabs.prototype.expandView = function() {
|
||||
var $gutterIcon;
|
||||
if ($.cookie('collapsed_gutter') === 'true') {
|
||||
if (Cookies.get('collapsed_gutter') === 'true') {
|
||||
return;
|
||||
}
|
||||
$gutterIcon = $('.js-sidebar-toggle i:visible');
|
||||
|
|
|
@ -101,6 +101,7 @@
|
|||
// display:block overrides the hide-collapse rule
|
||||
return $value.css('display', '');
|
||||
},
|
||||
vue: $dropdown.hasClass('js-issue-board-sidebar'),
|
||||
clicked: function(selected, $el, e) {
|
||||
var data, isIssueIndex, isMRIndex, page;
|
||||
page = $('body').data('page');
|
||||
|
@ -110,7 +111,7 @@
|
|||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if ($('html').hasClass('issue-boards-page')) {
|
||||
if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
|
||||
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name;
|
||||
gl.issueBoards.BoardsStore.updateFiltersUrl();
|
||||
e.preventDefault();
|
||||
|
@ -123,6 +124,24 @@
|
|||
return Issuable.filterResults($dropdown.closest('form'));
|
||||
} else if ($dropdown.hasClass('js-filter-submit')) {
|
||||
return $dropdown.closest('form').submit();
|
||||
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
|
||||
if (selected.id !== -1) {
|
||||
Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'milestone', new ListMilestone({
|
||||
id: selected.id,
|
||||
title: selected.name
|
||||
}));
|
||||
} else {
|
||||
Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'milestone');
|
||||
}
|
||||
|
||||
$dropdown.trigger('loading.gl.dropdown');
|
||||
$loading.fadeIn();
|
||||
|
||||
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
|
||||
.then(function () {
|
||||
$dropdown.trigger('loaded.gl.dropdown');
|
||||
$loading.fadeOut();
|
||||
});
|
||||
} else {
|
||||
selected = $selectbox.find('input[type="hidden"]').val();
|
||||
data = {};
|
||||
|
|
|
@ -2,36 +2,39 @@
|
|||
|
||||
class Pipelines {
|
||||
constructor() {
|
||||
$(document).off('click', '.toggle-pipeline-btn').on('click', '.toggle-pipeline-btn', this.toggleGraph);
|
||||
this.initGraphToggle();
|
||||
this.addMarginToBuildColumns();
|
||||
}
|
||||
|
||||
initGraphToggle() {
|
||||
this.pipelineGraph = document.querySelector('.pipeline-graph');
|
||||
this.toggleButton = document.querySelector('.toggle-pipeline-btn');
|
||||
this.toggleButtonText = this.toggleButton.querySelector('.toggle-btn-text');
|
||||
this.toggleButton.addEventListener('click', this.toggleGraph.bind(this));
|
||||
}
|
||||
|
||||
toggleGraph() {
|
||||
const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
|
||||
const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
|
||||
const $btnText = $(this).find('.toggle-btn-text');
|
||||
const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
|
||||
|
||||
$($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
|
||||
|
||||
|
||||
graphCollapsed ? $btnText.text('Hide') : $btnText.text('Expand')
|
||||
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() {
|
||||
const $secondChildBuildNode = $('.build:nth-child(2)');
|
||||
if ($secondChildBuildNode.length) {
|
||||
const $firstChildBuildNode = $secondChildBuildNode.prev('.build');
|
||||
const $multiBuildColumn = $secondChildBuildNode.closest('.stage-column');
|
||||
const $previousColumn = $multiBuildColumn.prev('.stage-column');
|
||||
$multiBuildColumn.addClass('left-margin');
|
||||
$firstChildBuildNode.addClass('left-connector');
|
||||
$previousColumn.each(function() {
|
||||
$this = $(this);
|
||||
if ($('.build', $this).length === 1) $this.addClass('no-margin');
|
||||
});
|
||||
const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)');
|
||||
for (buildNodeIndex in secondChildBuildNodes) {
|
||||
const buildNode = secondChildBuildNodes[buildNodeIndex];
|
||||
const firstChildBuildNode = buildNode.previousElementSibling;
|
||||
if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue;
|
||||
const multiBuildColumn = buildNode.closest('.stage-column');
|
||||
const previousColumn = multiBuildColumn.previousElementSibling;
|
||||
if (!previousColumn || !previousColumn.matches('.stage-column')) continue;
|
||||
multiBuildColumn.classList.add('left-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 @@
|
|||
return $(this).parents('form').submit();
|
||||
});
|
||||
$('.hide-no-ssh-message').on('click', function(e) {
|
||||
$.cookie('hide_no_ssh_message', 'false', {
|
||||
path: gon.relative_url_root || '/'
|
||||
});
|
||||
Cookies.set('hide_no_ssh_message', 'false');
|
||||
$(this).parents('.no-ssh-key-message').remove();
|
||||
return e.preventDefault();
|
||||
});
|
||||
$('.hide-no-password-message').on('click', function(e) {
|
||||
$.cookie('hide_no_password_message', 'false', {
|
||||
path: gon.relative_url_root || '/'
|
||||
});
|
||||
Cookies.set('hide_no_password_message', 'false');
|
||||
$(this).parents('.no-password-message').remove();
|
||||
return e.preventDefault();
|
||||
});
|
||||
|
@ -82,7 +78,7 @@
|
|||
if (ref.header != null) {
|
||||
return $('<li />').addClass('dropdown-header').text(ref.header);
|
||||
} else {
|
||||
link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
|
||||
link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', ref);
|
||||
return $('<li />').append(link);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -5,15 +5,24 @@
|
|||
function Sidebar(currentUser) {
|
||||
this.toggleTodo = bind(this.toggleTodo, this);
|
||||
this.sidebar = $('aside');
|
||||
this.removeListeners();
|
||||
this.addEventListeners();
|
||||
}
|
||||
|
||||
Sidebar.prototype.removeListeners = function () {
|
||||
this.sidebar.off('click', '.sidebar-collapsed-icon');
|
||||
$('.dropdown').off('hidden.gl.dropdown');
|
||||
$('.dropdown').off('loading.gl.dropdown');
|
||||
$('.dropdown').off('loaded.gl.dropdown');
|
||||
$(document).off('click', '.js-sidebar-toggle');
|
||||
}
|
||||
|
||||
Sidebar.prototype.addEventListeners = function() {
|
||||
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
|
||||
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
|
||||
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
|
||||
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
|
||||
$(document).off('click', '.js-sidebar-toggle').on('click', '.js-sidebar-toggle', function(e, triggered) {
|
||||
$(document).on('click', '.js-sidebar-toggle', function(e, triggered) {
|
||||
var $allGutterToggleIcons, $this, $thisIcon;
|
||||
e.preventDefault();
|
||||
$this = $(this);
|
||||
|
@ -29,9 +38,7 @@
|
|||
$('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
|
||||
}
|
||||
if (!triggered) {
|
||||
return $.cookie("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'), {
|
||||
path: gon.relative_url_root || '/'
|
||||
});
|
||||
return Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
|
||||
}
|
||||
});
|
||||
return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo);
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
}
|
||||
|
||||
init() {
|
||||
this.isPinned = $.cookie(pinnedStateCookie) === 'true';
|
||||
this.isPinned = Cookies.get(pinnedStateCookie) === 'true';
|
||||
this.isExpanded = (
|
||||
window.innerWidth >= sidebarBreakpoint &&
|
||||
$(pageSelector).hasClass(expandedPageClass)
|
||||
|
@ -62,10 +62,7 @@
|
|||
if (!this.isPinned) {
|
||||
this.isExpanded = false;
|
||||
}
|
||||
$.cookie(pinnedStateCookie, this.isPinned ? 'true' : 'false', {
|
||||
path: gon.relative_url_root || '/',
|
||||
expires: 3650
|
||||
});
|
||||
Cookies.set(pinnedStateCookie, this.isPinned ? 'true' : 'false', { expires: 3650 });
|
||||
this.renderState();
|
||||
}
|
||||
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
function Subscription(container) {
|
||||
this.toggleSubscription = bind(this.toggleSubscription, this);
|
||||
var $container;
|
||||
$container = $(container);
|
||||
this.url = $container.attr('data-url');
|
||||
this.subscribe_button = $container.find('.js-subscribe-button');
|
||||
this.subscription_status = $container.find('.subscription-status');
|
||||
this.$container = $(container);
|
||||
this.url = this.$container.attr('data-url');
|
||||
this.subscribe_button = this.$container.find('.js-subscribe-button');
|
||||
this.subscription_status = this.$container.find('.subscription-status');
|
||||
this.subscribe_button.unbind('click').click(this.toggleSubscription);
|
||||
}
|
||||
|
||||
|
@ -18,17 +18,27 @@
|
|||
action = btn.find('span').text();
|
||||
current_status = this.subscription_status.attr('data-status');
|
||||
btn.addClass('disabled');
|
||||
|
||||
if ($('html').hasClass('issue-boards-page')) {
|
||||
this.url = this.$container.attr('data-url');
|
||||
}
|
||||
|
||||
return $.post(this.url, (function(_this) {
|
||||
return function() {
|
||||
var status;
|
||||
btn.removeClass('disabled');
|
||||
status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed';
|
||||
_this.subscription_status.attr('data-status', status);
|
||||
action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe';
|
||||
btn.find('span').text(action);
|
||||
_this.subscription_status.find('>div').toggleClass('hidden');
|
||||
if (btn.attr('data-original-title')) {
|
||||
return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle');
|
||||
|
||||
if ($('html').hasClass('issue-boards-page')) {
|
||||
Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'subscribed', !gl.issueBoards.BoardsStore.detail.issue.subscribed);
|
||||
} else {
|
||||
status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed';
|
||||
_this.subscription_status.attr('data-status', status);
|
||||
action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe';
|
||||
btn.find('span').text(action);
|
||||
_this.subscription_status.find('>div').toggleClass('hidden');
|
||||
if (btn.attr('data-original-title')) {
|
||||
return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle');
|
||||
}
|
||||
}
|
||||
};
|
||||
})(this));
|
||||
|
|
|
@ -32,24 +32,22 @@
|
|||
this.currentTemplate = currentTemplate;
|
||||
if (err) return; // Error handled by global AJAX error handler
|
||||
this.stopLoadingSpinner();
|
||||
this.setInputValueToTemplateContent(true);
|
||||
this.setInputValueToTemplateContent();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setInputValueToTemplateContent(append) {
|
||||
setInputValueToTemplateContent() {
|
||||
// `this.requestFileSuccess` sets the value of the description input field
|
||||
// to the content of the template selected. If `append` is true, the
|
||||
// template content will be appended to the previous value of the field,
|
||||
// separated by a blank line if the previous value is non-empty.
|
||||
// to the content of the template selected.
|
||||
if (this.titleInput.val() === '') {
|
||||
// If the title has not yet been set, focus the title input and
|
||||
// skip focusing the description input by setting `true` as the
|
||||
// `skipFocus` option to `requestFileSuccess`.
|
||||
this.requestFileSuccess(this.currentTemplate, {skipFocus: true, append});
|
||||
this.requestFileSuccess(this.currentTemplate, {skipFocus: true});
|
||||
this.titleInput.focus();
|
||||
} else {
|
||||
this.requestFileSuccess(this.currentTemplate, {skipFocus: false, append});
|
||||
this.requestFileSuccess(this.currentTemplate, {skipFocus: false});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -23,10 +23,7 @@
|
|||
hideProjectLimitMessage() {
|
||||
$('.hide-project-limit-message').on('click', e => {
|
||||
e.preventDefault();
|
||||
const path = gon.relative_url_root || '/';
|
||||
$.cookie('hide_project_limit_message', 'false', {
|
||||
path: path
|
||||
});
|
||||
Cookies.set('hide_project_limit_message', 'false');
|
||||
$(this).parents('.project-limit-message').remove();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,7 +9,11 @@
|
|||
this.usersPath = "/autocomplete/users.json";
|
||||
this.userPath = "/autocomplete/users/:id.json";
|
||||
if (currentUser != null) {
|
||||
this.currentUser = JSON.parse(currentUser);
|
||||
if (typeof currentUser === 'object') {
|
||||
this.currentUser = currentUser;
|
||||
} else {
|
||||
this.currentUser = JSON.parse(currentUser);
|
||||
}
|
||||
}
|
||||
$('.js-user-search').each((function(_this) {
|
||||
return function(i, dropdown) {
|
||||
|
@ -32,9 +36,30 @@
|
|||
$value = $block.find('.value');
|
||||
$collapsedSidebar = $block.find('.sidebar-collapsed-user');
|
||||
$loading = $block.find('.block-loading').fadeOut();
|
||||
|
||||
var updateIssueBoardsIssue = function () {
|
||||
$loading.fadeIn();
|
||||
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
|
||||
.then(function () {
|
||||
$loading.fadeOut();
|
||||
});
|
||||
};
|
||||
|
||||
$block.on('click', '.js-assign-yourself', function(e) {
|
||||
e.preventDefault();
|
||||
return assignTo(_this.currentUser.id);
|
||||
|
||||
if ($dropdown.hasClass('js-issue-board-sidebar')) {
|
||||
Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({
|
||||
id: _this.currentUser.id,
|
||||
username: _this.currentUser.username,
|
||||
name: _this.currentUser.name,
|
||||
avatar_url: _this.currentUser.avatar_url
|
||||
}));
|
||||
|
||||
updateIssueBoardsIssue();
|
||||
} else {
|
||||
return assignTo(_this.currentUser.id);
|
||||
}
|
||||
});
|
||||
assignTo = function(selected) {
|
||||
var data;
|
||||
|
@ -150,6 +175,7 @@
|
|||
// display:block overrides the hide-collapse rule
|
||||
return $value.css('display', '');
|
||||
},
|
||||
vue: $dropdown.hasClass('js-issue-board-sidebar'),
|
||||
clicked: function(user, $el, e) {
|
||||
var isIssueIndex, isMRIndex, page, selected;
|
||||
page = $('body').data('page');
|
||||
|
@ -160,7 +186,7 @@
|
|||
selectedId = user.id;
|
||||
return;
|
||||
}
|
||||
if ($('html').hasClass('issue-boards-page')) {
|
||||
if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
|
||||
selectedId = user.id;
|
||||
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
|
||||
gl.issueBoards.BoardsStore.updateFiltersUrl();
|
||||
|
@ -170,6 +196,19 @@
|
|||
return Issuable.filterResults($dropdown.closest('form'));
|
||||
} else if ($dropdown.hasClass('js-filter-submit')) {
|
||||
return $dropdown.closest('form').submit();
|
||||
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
|
||||
if (user.id) {
|
||||
Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
avatar_url: user.avatar_url
|
||||
}));
|
||||
} else {
|
||||
Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'assignee');
|
||||
}
|
||||
|
||||
updateIssueBoardsIssue();
|
||||
} else {
|
||||
selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val();
|
||||
return assignTo(selected);
|
||||
|
|
|
@ -227,7 +227,7 @@ header {
|
|||
float: none !important;
|
||||
|
||||
.visible-xs,
|
||||
.visable-sm {
|
||||
.visible-sm {
|
||||
display: table-cell !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,6 +45,15 @@
|
|||
.page-with-sidebar {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.issues-filters {
|
||||
position: relative;
|
||||
z-index: 999999;
|
||||
}
|
||||
}
|
||||
|
||||
.boards-app {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.boards-app-loading {
|
||||
|
@ -66,6 +75,10 @@
|
|||
height: 475px; // Needed for PhantomJS
|
||||
height: calc(100vh - 220px);
|
||||
min-height: 475px;
|
||||
|
||||
&.is-compact {
|
||||
width: calc(100% - 290px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -184,6 +197,10 @@
|
|||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: $row-hover;
|
||||
}
|
||||
|
||||
.label {
|
||||
border: 0;
|
||||
outline: 0;
|
||||
|
@ -212,6 +229,10 @@
|
|||
margin-right: 5px;
|
||||
font-size: (14px / $issue-boards-font-size) * 1em;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card-number {
|
||||
|
@ -264,3 +285,48 @@
|
|||
border-width: 1px 0 1px 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.issue-boards-sidebar {
|
||||
&.right-sidebar {
|
||||
top: 153px;
|
||||
bottom: 0;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
top: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
.issuable-sidebar-header {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gutter-toggle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 15px;
|
||||
right: 0;
|
||||
width: 22px;
|
||||
color: $gray-darkest;
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-top: (-11px / 2);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
path {
|
||||
fill: $gray-darkest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.issuable-header-text {
|
||||
width: 100%;
|
||||
padding-right: 35px;
|
||||
|
||||
> strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,10 +73,13 @@ module Projects
|
|||
def serialize_as_json(resource)
|
||||
resource.as_json(
|
||||
labels: true,
|
||||
only: [:iid, :title, :confidential],
|
||||
only: [:iid, :title, :confidential, :due_date],
|
||||
include: {
|
||||
assignee: { only: [:id, :name, :username], methods: [:avatar_url] }
|
||||
})
|
||||
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
|
||||
milestone: { only: [:id, :title] }
|
||||
},
|
||||
user: current_user
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -50,7 +50,7 @@ class LabelsFinder < UnionFinder
|
|||
end
|
||||
|
||||
def projects_ids
|
||||
params[:project_ids].presence
|
||||
params[:project_ids]
|
||||
end
|
||||
|
||||
def title
|
||||
|
|
|
@ -5,7 +5,7 @@ module SidekiqHelper
|
|||
(?<mem>[\d\.,]+)\s+
|
||||
(?<state>[DRSTWXZNLsl\+<]+)\s+
|
||||
(?<start>.+)\s+
|
||||
(?<command>sidekiq.*\])\s+
|
||||
(?<command>sidekiq.*\])\s*
|
||||
\z/x
|
||||
|
||||
def parse_sidekiq_ps(line)
|
||||
|
|
|
@ -287,10 +287,12 @@ class Issue < ActiveRecord::Base
|
|||
|
||||
def as_json(options = {})
|
||||
super(options).tap do |json|
|
||||
json[:subscribed] = subscribed?(options[:user]) if options.has_key?(:user)
|
||||
|
||||
if options.has_key?(:labels)
|
||||
json[:labels] = labels.as_json(
|
||||
project: project,
|
||||
only: [:id, :title, :description, :color],
|
||||
only: [:id, :title, :description, :color, :priority],
|
||||
methods: [:text_color]
|
||||
)
|
||||
end
|
||||
|
|
|
@ -461,7 +461,7 @@
|
|||
.panel-body
|
||||
= lorem
|
||||
|
||||
%h2#alert Alerts
|
||||
%h2#alerts Alerts
|
||||
|
||||
.row
|
||||
.col-md-6
|
||||
|
|
|
@ -7,8 +7,11 @@
|
|||
":issue-link-base" => "issueLinkBase",
|
||||
":disabled" => "disabled",
|
||||
"track-by" => "id" }
|
||||
%li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id }",
|
||||
":index" => "index" }
|
||||
%li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }",
|
||||
":index" => "index",
|
||||
"@mousedown" => "mouseDown",
|
||||
"@mouseMove" => "mouseMove",
|
||||
"@mouseup" => "showIssue($event)" }
|
||||
%h4.card-title
|
||||
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
|
||||
%a{ ":href" => "issueLinkBase + '/' + issue.id",
|
||||
|
@ -18,6 +21,11 @@
|
|||
%span.card-number{ "v-if" => "issue.id" }
|
||||
= precede '#' do
|
||||
{{ issue.id }}
|
||||
%a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username",
|
||||
":title" => "'Assigned to ' + issue.assignee.name",
|
||||
"v-if" => "issue.assignee",
|
||||
data: { container: 'body' } }
|
||||
%img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
|
||||
%button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
|
||||
type: "button",
|
||||
"v-if" => "(!list.label || label.id !== list.label.id)",
|
||||
|
@ -26,8 +34,3 @@
|
|||
":title" => "label.description",
|
||||
data: { container: 'body' } }
|
||||
{{ label.title }}
|
||||
%a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username",
|
||||
":title" => "'Assigned to ' + issue.assignee.name",
|
||||
"v-if" => "issue.assignee",
|
||||
data: { container: 'body' } }
|
||||
%img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
%board-sidebar{ "inline-template" => true,
|
||||
":current-user" => "#{current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) if current_user}" }
|
||||
%aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
|
||||
.issuable-sidebar
|
||||
.block.issuable-sidebar-header
|
||||
%span.issuable-header-text.hide-collapsed.pull-left
|
||||
%strong
|
||||
{{ issue.title }}
|
||||
%br/
|
||||
%span
|
||||
= precede "#" do
|
||||
{{ issue.id }}
|
||||
%a.gutter-toggle.pull-right{ role: "button",
|
||||
href: "#",
|
||||
"@click.prevent" => "closeSidebar",
|
||||
"aria-label" => "Toggle sidebar" }
|
||||
= custom_icon("icon_close", size: 15)
|
||||
.js-issuable-update
|
||||
= render "projects/boards/components/sidebar/assignee"
|
||||
= render "projects/boards/components/sidebar/milestone"
|
||||
= render "projects/boards/components/sidebar/due_date"
|
||||
= render "projects/boards/components/sidebar/labels"
|
||||
= render "projects/boards/components/sidebar/notifications"
|
|
@ -0,0 +1,40 @@
|
|||
.block.assignee
|
||||
.title.hide-collapsed
|
||||
Assignee
|
||||
= icon("spinner spin", class: "block-loading")
|
||||
- if can?(current_user, :admin_issue, @project)
|
||||
= link_to "Edit", "#", class: "edit-link pull-right"
|
||||
.value.hide-collapsed
|
||||
%span.assign-yourself.no-value{ "v-if" => "!issue.assignee" }
|
||||
No assignee
|
||||
- if can?(current_user, :admin_issue, @project)
|
||||
\-
|
||||
%a.js-assign-yourself{ href: "#" }
|
||||
assign yourself
|
||||
%a.author_link.bold{ ":href" => "'#{root_url}' + issue.assignee.username",
|
||||
"v-if" => "issue.assignee" }
|
||||
%img.avatar.avatar-inline.s32{ ":src" => "issue.assignee.avatar",
|
||||
width: "32" }
|
||||
%span.author
|
||||
{{ issue.assignee.name }}
|
||||
%span.username
|
||||
= precede "@" do
|
||||
{{ issue.assignee.username }}
|
||||
- if can?(current_user, :admin_issue, @project)
|
||||
.selectbox.hide-collapsed
|
||||
%input{ type: "hidden",
|
||||
name: "issue[assignee_id]",
|
||||
id: "issue_assignee_id",
|
||||
":value" => "issue.assignee.id",
|
||||
"v-if" => "issue.assignee" }
|
||||
.dropdown
|
||||
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", field_name: "issue[assignee_id]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true" },
|
||||
":data-issuable-id" => "issue.id",
|
||||
":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
|
||||
Select assignee
|
||||
= icon("chevron-down")
|
||||
.dropdown-menu.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
|
||||
= dropdown_title("Assign to")
|
||||
= dropdown_filter("Search users")
|
||||
= dropdown_content
|
||||
= dropdown_loading
|
|
@ -0,0 +1,32 @@
|
|||
.block.due_date
|
||||
.title
|
||||
Due date
|
||||
= icon("spinner spin", class: "block-loading")
|
||||
- if can?(current_user, :admin_issue, @project)
|
||||
= link_to "Edit", "#", class: "edit-link pull-right"
|
||||
.value
|
||||
.value-content
|
||||
%span.no-value{ "v-if" => "!issue.dueDate" }
|
||||
No due date
|
||||
%span.bold{ "v-if" => "issue.dueDate" }
|
||||
{{ issue.dueDate | due-date }}
|
||||
- if can?(current_user, :admin_issue, @project)
|
||||
%span.no-value.js-remove-due-date-holder{ "v-if" => "issue.dueDate" }
|
||||
\-
|
||||
%a.js-remove-due-date{ href: "#", role: "button" }
|
||||
remove due date
|
||||
- if can?(current_user, :admin_issue, @project)
|
||||
.selectbox
|
||||
%input{ type: "hidden",
|
||||
name: "issue[due_date]",
|
||||
":value" => "issue.dueDate" }
|
||||
.dropdown
|
||||
%button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button',
|
||||
data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" },
|
||||
":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
|
||||
%span.dropdown-toggle-text Due date
|
||||
= icon('chevron-down')
|
||||
.dropdown-menu.dropdown-menu-due-date
|
||||
= dropdown_title('Due date')
|
||||
= dropdown_content do
|
||||
.js-due-date-calendar
|
|
@ -0,0 +1,30 @@
|
|||
.block.labels
|
||||
.title
|
||||
Labels
|
||||
= icon("spinner spin", class: "block-loading")
|
||||
- if can?(current_user, :admin_issue, @project)
|
||||
= link_to "Edit", "#", class: "edit-link pull-right"
|
||||
.value.issuable-show-labels
|
||||
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
|
||||
None
|
||||
%a{ href: "#",
|
||||
"v-for" => "label in issue.labels" }
|
||||
%span.label.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
|
||||
{{ label.title }}
|
||||
- if can?(current_user, :admin_issue, @project)
|
||||
.selectbox
|
||||
%input{ type: "hidden",
|
||||
name: "issue[label_names][]",
|
||||
"v-for" => "label in issue.labels",
|
||||
":value" => "label.id" }
|
||||
.dropdown
|
||||
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button",
|
||||
data: { toggle: "dropdown", field_name: "issue[label_names][]", show_no: "true", show_any: "true", project_id: @project.id, labels: namespace_project_labels_path(@project.namespace, @project, :json), namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) },
|
||||
":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
|
||||
%span.dropdown-toggle-text
|
||||
Label
|
||||
= icon('chevron-down')
|
||||
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
|
||||
= render partial: "shared/issuable/label_page_default"
|
||||
- if can? current_user, :admin_label, @project and @project
|
||||
= render partial: "shared/issuable/label_page_create"
|
|
@ -0,0 +1,28 @@
|
|||
.block.milestone
|
||||
.title
|
||||
Milestone
|
||||
= icon("spinner spin", class: "block-loading")
|
||||
- if can?(current_user, :admin_issue, @project)
|
||||
= link_to "Edit", "#", class: "edit-link pull-right"
|
||||
.value
|
||||
%span.no-value{ "v-if" => "!issue.milestone" }
|
||||
None
|
||||
%span.bold.has-tooltip{ "v-if" => "issue.milestone" }
|
||||
{{ issue.milestone.title }}
|
||||
- if can?(current_user, :admin_issue, @project)
|
||||
.selectbox
|
||||
%input{ type: "hidden",
|
||||
":value" => "issue.milestone.id",
|
||||
name: "issue[milestone_id]",
|
||||
"v-if" => "issue.milestone" }
|
||||
.dropdown
|
||||
%button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: "issue", use_id: "true" },
|
||||
":data-issuable-id" => "issue.id",
|
||||
":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
|
||||
Milestone
|
||||
= icon("chevron-down")
|
||||
.dropdown-menu.dropdown-select.dropdown-menu-selectable
|
||||
= dropdown_title("Assignee milestone")
|
||||
= dropdown_filter("Search milestones")
|
||||
= dropdown_content
|
||||
= dropdown_loading
|
|
@ -0,0 +1,11 @@
|
|||
- if current_user
|
||||
.block.light.subscription{ ":data-url" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '/toggle_subscription'" }
|
||||
.title
|
||||
Notifications
|
||||
%button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
|
||||
{{ issue.subscribed ? 'Unsubscribe' : 'Subscribe' }}
|
||||
.subscription-status{ ":data-status" => "issue.subscribed ? 'subscribed' : 'unsubscribed'" }
|
||||
.unsubscribed{ "v-show" => "!issue.subscribed" }
|
||||
You're not receiving notifications from this thread.
|
||||
.subscribed{ "v-show" => "issue.subscribed" }
|
||||
You're receiving notifications because you're subscribed to this thread.
|
|
@ -10,7 +10,9 @@
|
|||
|
||||
= render 'shared/issuable/filter', type: :boards
|
||||
|
||||
.boards-list#board-app{ "v-cloak" => true, data: board_data }
|
||||
.boards-app-loading.text-center{ "v-if" => "loading" }
|
||||
= icon("spinner spin")
|
||||
= render "projects/boards/components/board"
|
||||
#board-app.boards-app{ "v-cloak" => true, data: board_data }
|
||||
.boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
|
||||
.boards-app-loading.text-center{ "v-if" => "loading" }
|
||||
= icon("spinner spin")
|
||||
= render "projects/boards/components/board"
|
||||
= render "projects/boards/components/sidebar"
|
||||
|
|
|
@ -10,7 +10,9 @@
|
|||
|
||||
= render 'shared/issuable/filter', type: :boards
|
||||
|
||||
.boards-list#board-app{ "v-cloak" => true, data: board_data }
|
||||
.boards-app-loading.text-center{ "v-if" => "loading" }
|
||||
= icon("spinner spin")
|
||||
= render "projects/boards/components/board"
|
||||
#board-app.boards-app{ "v-cloak" => true, data: board_data }
|
||||
.boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
|
||||
.boards-app-loading.text-center{ "v-if" => "loading" }
|
||||
= icon("spinner spin")
|
||||
= render "projects/boards/components/board"
|
||||
= render "projects/boards/components/sidebar"
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="M9,7.5l5.83-5.91a.48.48,0,0,0,0-.69L14.11.15a.46.46,0,0,0-.68,0l-5.93,6L1.57.15a.46.46,0,0,0-.68,0L.15.9a.48.48,0,0,0,0,.69L6,7.5.15,13.41a.48.48,0,0,0,0,.69l.74.75a.46.46,0,0,0,.68,0l5.93-6,5.93,6a.46.46,0,0,0,.68,0l.74-.75a.48.48,0,0,0,0-.69Z"/></svg>
|
After Width: | Height: | Size: 322 B |
|
@ -9,6 +9,18 @@ class ProjectCacheWorker
|
|||
|
||||
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)
|
||||
if try_obtain_lease_for(project_id)
|
||||
Rails.logger.
|
||||
|
@ -37,8 +49,6 @@ class ProjectCacheWorker
|
|||
end
|
||||
|
||||
def try_obtain_lease_for(project_id)
|
||||
Gitlab::ExclusiveLease.
|
||||
new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT).
|
||||
try_obtain
|
||||
self.class.lease_for(project_id).try_obtain
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
## 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
|
||||
contributing to documentation.
|
||||
- [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
# 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.
|
|||
Here is a list of must-have items. Use them in the exact order that appears
|
||||
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:
|
||||
|
||||
```
|
||||
|
|
|
@ -142,6 +142,9 @@ gitlab-workhorse we need a Go compiler. The instructions below assume you
|
|||
use 64-bit Linux. You can find downloads for other platforms at the [Go download
|
||||
page](https://golang.org/dl).
|
||||
|
||||
# Remove former Go installation folder
|
||||
sudo rm -rf /usr/local/go
|
||||
|
||||
curl --remote-name --progress https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz
|
||||
echo '43afe0c5017e502630b1aea4d44b8a7f059bf60d7f29dfd58db454d4e4e0ae53 go1.5.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
|
||||
sudo tar -C /usr/local -xzf go1.5.3.linux-amd64.tar.gz
|
||||
|
|
|
@ -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.
|
||||
It uses the [Fog library](http://fog.io/) to perform the upload.
|
||||
In the example below we use Amazon S3 for storage.
|
||||
Fog also supports [other storage providers](http://fog.io/storage/).
|
||||
In the example below we use Amazon S3 for storage, but Fog also lets you use
|
||||
[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:
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-13-stable-ee
|
|||
```bash
|
||||
cd /home/git/gitlab-shell
|
||||
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
|
||||
|
|
|
@ -49,18 +49,23 @@ module API
|
|||
attrs = attributes_for_keys [:title, :key]
|
||||
attrs[:key].strip! if attrs[:key]
|
||||
|
||||
# Check for an existing key joined to this project
|
||||
key = user_project.deploy_keys.find_by(key: attrs[:key])
|
||||
present key, with: Entities::SSHKey if key
|
||||
if key
|
||||
present key, with: Entities::SSHKey
|
||||
break
|
||||
end
|
||||
|
||||
# Check for available deploy keys in other projects
|
||||
key = current_user.accessible_deploy_keys.find_by(key: attrs[:key])
|
||||
if key
|
||||
user_project.deploy_keys << key
|
||||
present key, with: Entities::SSHKey
|
||||
break
|
||||
end
|
||||
|
||||
# Create a new deploy key
|
||||
key = DeployKey.new attrs
|
||||
|
||||
if key.valid? && user_project.deploy_keys << key
|
||||
present key, with: Entities::SSHKey
|
||||
else
|
||||
|
|
|
@ -4,25 +4,24 @@ module API
|
|||
before { authenticate! }
|
||||
before { authorize! :download_code, user_project }
|
||||
|
||||
params do
|
||||
requires :id, type: String, desc: 'The ID of a project'
|
||||
end
|
||||
resource :projects do
|
||||
# Get a project repository tags
|
||||
#
|
||||
# Parameters:
|
||||
# id (required) - The ID of a project
|
||||
# Example Request:
|
||||
# GET /projects/:id/repository/tags
|
||||
desc 'Get a project repository tags' do
|
||||
success Entities::RepoTag
|
||||
end
|
||||
get ":id/repository/tags" do
|
||||
present user_project.repository.tags.sort_by(&:name).reverse,
|
||||
with: Entities::RepoTag, project: user_project
|
||||
end
|
||||
|
||||
# Get a single repository tag
|
||||
#
|
||||
# Parameters:
|
||||
# id (required) - The ID of a project
|
||||
# tag_name (required) - The name of the tag
|
||||
# Example Request:
|
||||
# GET /projects/:id/repository/tags/:tag_name
|
||||
desc 'Get a single repository tag' do
|
||||
success Entities::RepoTag
|
||||
end
|
||||
params do
|
||||
requires :tag_name, type: String, desc: 'The name of the tag'
|
||||
end
|
||||
get ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do
|
||||
tag = user_project.repository.find_tag(params[:tag_name])
|
||||
not_found!('Tag') unless tag
|
||||
|
@ -30,20 +29,21 @@ module API
|
|||
present tag, with: Entities::RepoTag, project: user_project
|
||||
end
|
||||
|
||||
# Create tag
|
||||
#
|
||||
# Parameters:
|
||||
# id (required) - The ID of a project
|
||||
# tag_name (required) - The name of the tag
|
||||
# ref (required) - Create tag from commit sha or branch
|
||||
# message (optional) - Specifying a message creates an annotated tag.
|
||||
# Example Request:
|
||||
# POST /projects/:id/repository/tags
|
||||
desc 'Create a new repository tag' do
|
||||
success Entities::RepoTag
|
||||
end
|
||||
params do
|
||||
requires :tag_name, type: String, desc: 'The name of the tag'
|
||||
requires :ref, type: String, desc: 'The commit sha or branch name'
|
||||
optional :message, type: String, desc: 'Specifying a message creates an annotated tag'
|
||||
optional :release_description, type: String, desc: 'Specifying release notes stored in the GitLab database'
|
||||
end
|
||||
post ':id/repository/tags' do
|
||||
authorize_push_project
|
||||
message = params[:message] || nil
|
||||
create_params = declared(params)
|
||||
|
||||
result = CreateTagService.new(user_project, current_user).
|
||||
execute(params[:tag_name], params[:ref], message, params[:release_description])
|
||||
execute(create_params[:tag_name], create_params[:ref], create_params[:message], create_params[:release_description])
|
||||
|
||||
if result[:status] == :success
|
||||
present result[:tag],
|
||||
|
@ -54,15 +54,13 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
# Delete tag
|
||||
#
|
||||
# Parameters:
|
||||
# id (required) - The ID of a project
|
||||
# tag_name (required) - The name of the tag
|
||||
# Example Request:
|
||||
# DELETE /projects/:id/repository/tags/:tag
|
||||
desc 'Delete a repository tag'
|
||||
params do
|
||||
requires :tag_name, type: String, desc: 'The name of the tag'
|
||||
end
|
||||
delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do
|
||||
authorize_push_project
|
||||
|
||||
result = DeleteTagService.new(user_project, current_user).
|
||||
execute(params[:tag_name])
|
||||
|
||||
|
@ -75,17 +73,16 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
# Add release notes to tag
|
||||
#
|
||||
# Parameters:
|
||||
# id (required) - The ID of a project
|
||||
# tag_name (required) - The name of the tag
|
||||
# description (required) - Release notes with markdown support
|
||||
# Example Request:
|
||||
# POST /projects/:id/repository/tags/:tag_name/release
|
||||
desc 'Add a release note to a tag' do
|
||||
success Entities::Release
|
||||
end
|
||||
params do
|
||||
requires :tag_name, type: String, desc: 'The name of the tag'
|
||||
requires :description, type: String, desc: 'Release notes with markdown support'
|
||||
end
|
||||
post ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do
|
||||
authorize_push_project
|
||||
required_attributes! [:description]
|
||||
|
||||
result = CreateReleaseService.new(user_project, current_user).
|
||||
execute(params[:tag_name], params[:description])
|
||||
|
||||
|
@ -96,17 +93,16 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
# Updates a release notes of a tag
|
||||
#
|
||||
# Parameters:
|
||||
# id (required) - The ID of a project
|
||||
# tag_name (required) - The name of the tag
|
||||
# description (required) - Release notes with markdown support
|
||||
# Example Request:
|
||||
# PUT /projects/:id/repository/tags/:tag_name/release
|
||||
desc "Update a tag's release note" do
|
||||
success Entities::Release
|
||||
end
|
||||
params do
|
||||
requires :tag_name, type: String, desc: 'The name of the tag'
|
||||
requires :description, type: String, desc: 'Release notes with markdown support'
|
||||
end
|
||||
put ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do
|
||||
authorize_push_project
|
||||
required_attributes! [:description]
|
||||
|
||||
result = UpdateReleaseService.new(user_project, current_user).
|
||||
execute(params[:tag_name], params[:description])
|
||||
|
||||
|
|
|
@ -333,11 +333,11 @@ module API
|
|||
user = User.find_by(id: declared(params).id)
|
||||
not_found!('User') unless user
|
||||
|
||||
events = user.recent_events.
|
||||
events = user.events.
|
||||
merge(ProjectsFinder.new.execute(current_user)).
|
||||
references(:project).
|
||||
with_associations.
|
||||
page(params[:page])
|
||||
recent
|
||||
|
||||
present paginate(events), with: Entities::Event
|
||||
end
|
||||
|
|
|
@ -27,7 +27,7 @@ module Gitlab
|
|||
# on begin/ensure blocks to cancel a lease, because the 'ensure' does
|
||||
# not always run. Think of 'kill -9' from the Unicorn master for
|
||||
# instance.
|
||||
#
|
||||
#
|
||||
# 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
|
||||
# appropriate is to only use a lease for bulk/automated operations, and
|
||||
|
@ -48,6 +48,13 @@ module Gitlab
|
|||
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!
|
||||
|
||||
private
|
||||
|
|
|
@ -21,9 +21,11 @@ describe Projects::Boards::IssuesController do
|
|||
context 'with valid list id' do
|
||||
it 'returns issues that have the list label applied' do
|
||||
johndoe = create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png')))
|
||||
issue = create(:labeled_issue, project: project, labels: [planning])
|
||||
create(:labeled_issue, project: project, labels: [planning])
|
||||
create(:labeled_issue, project: project, labels: [development])
|
||||
create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
|
||||
create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
|
||||
issue.subscribe(johndoe)
|
||||
|
||||
list_issues user: user, board: board, list: list2
|
||||
|
||||
|
|
|
@ -116,31 +116,100 @@ describe SnippetsController do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET #raw' do
|
||||
let(:user) { create(:user) }
|
||||
%w(raw download).each do |action|
|
||||
describe "GET #{action}" do
|
||||
context 'when the personal snippet is private' do
|
||||
let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
|
||||
|
||||
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 signed in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
context 'when signed in user is not the author' do
|
||||
let(:other_author) { create(:author) }
|
||||
let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
|
||||
|
||||
context 'when signed in user is not the author' do
|
||||
let(:other_author) { create(:author) }
|
||||
let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
|
||||
it 'responds with status 404' do
|
||||
get action, id: other_personal_snippet.to_param
|
||||
|
||||
it 'responds with status 404' do
|
||||
get :raw, id: other_personal_snippet.to_param
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
context 'when signed in user is the author' do
|
||||
before { get action, id: personal_snippet.to_param }
|
||||
|
||||
it 'responds with status 200' do
|
||||
expect(assigns(:snippet)).to eq(personal_snippet)
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'has expected headers' do
|
||||
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
|
||||
|
||||
if action == :download
|
||||
expect(response.header['Content-Disposition']).to match(/attachment/)
|
||||
elsif action == :raw
|
||||
expect(response.header['Content-Disposition']).to match(/inline/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed in user is the author' do
|
||||
it 'renders the raw snippet' do
|
||||
get :raw, id: personal_snippet.to_param
|
||||
context 'when not signed in' do
|
||||
it 'redirects to the sign in page' do
|
||||
get action, id: personal_snippet.to_param
|
||||
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the personal snippet is internal' do
|
||||
let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
|
||||
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'responds with status 200' do
|
||||
get action, id: personal_snippet.to_param
|
||||
|
||||
expect(assigns(:snippet)).to eq(personal_snippet)
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
it 'redirects to the sign in page' do
|
||||
get action, id: personal_snippet.to_param
|
||||
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the personal snippet is public' do
|
||||
let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
|
||||
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'responds with status 200' do
|
||||
get action, id: personal_snippet.to_param
|
||||
|
||||
expect(assigns(:snippet)).to eq(personal_snippet)
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
it 'responds with status 200' do
|
||||
get action, id: personal_snippet.to_param
|
||||
|
||||
expect(assigns(:snippet)).to eq(personal_snippet)
|
||||
expect(response).to have_http_status(200)
|
||||
|
@ -148,84 +217,25 @@ describe SnippetsController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
it 'redirects to the sign in page' do
|
||||
get :raw, id: personal_snippet.to_param
|
||||
context 'when the personal snippet does not exist' do
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
it 'responds with status 404' do
|
||||
get action, id: 'doesntexist'
|
||||
|
||||
context 'when the personal snippet is internal' do
|
||||
let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
|
||||
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
it 'renders the raw snippet' do
|
||||
get :raw, id: personal_snippet.to_param
|
||||
context 'when not signed in' do
|
||||
it 'responds with status 404' do
|
||||
get action, id: 'doesntexist'
|
||||
|
||||
expect(assigns(:snippet)).to eq(personal_snippet)
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
it 'redirects to the sign in page' do
|
||||
get :raw, id: personal_snippet.to_param
|
||||
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the personal snippet is public' do
|
||||
let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
|
||||
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'renders the raw snippet' do
|
||||
get :raw, id: personal_snippet.to_param
|
||||
|
||||
expect(assigns(:snippet)).to eq(personal_snippet)
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
it 'renders the raw snippet' do
|
||||
get :raw, id: personal_snippet.to_param
|
||||
|
||||
expect(assigns(:snippet)).to eq(personal_snippet)
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the personal snippet does not exist' do
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'responds with status 404' do
|
||||
get :raw, id: 'doesntexist'
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
it 'responds with status 404' do
|
||||
get :raw, id: 'doesntexist'
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -66,6 +66,21 @@ describe 'Issue Boards new issue', feature: true, js: true do
|
|||
expect(page).to have_content('1')
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows sidebar when creating new issue' do
|
||||
page.within(first('.board')) do
|
||||
find('.board-issue-count-holder .btn').click
|
||||
end
|
||||
|
||||
page.within(first('.board-new-issue-form')) do
|
||||
find('.form-control').set('bug')
|
||||
click_button 'Submit issue'
|
||||
end
|
||||
|
||||
wait_for_vue_resource
|
||||
|
||||
expect(page).to have_selector('.issue-boards-sidebar')
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthorized user' do
|
||||
|
|
|
@ -0,0 +1,312 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe 'Issue Boards', feature: true, js: true do
|
||||
include WaitForAjax
|
||||
include WaitForVueResource
|
||||
|
||||
let(:project) { create(:empty_project, :public) }
|
||||
let(:board) { create(:board, project: project) }
|
||||
let(:user) { create(:user) }
|
||||
let!(:label) { create(:label, project: project) }
|
||||
let!(:label2) { create(:label, project: project) }
|
||||
let!(:milestone) { create(:milestone, project: project) }
|
||||
let!(:issue2) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [label]) }
|
||||
let!(:issue) { create(:issue, project: project) }
|
||||
|
||||
before do
|
||||
project.team << [user, :master]
|
||||
|
||||
login_as(user)
|
||||
|
||||
visit namespace_project_board_path(project.namespace, project, board)
|
||||
wait_for_vue_resource
|
||||
end
|
||||
|
||||
it 'shows sidebar when clicking issue' do
|
||||
page.within(first('.board')) do
|
||||
first('.card').click
|
||||
end
|
||||
|
||||
expect(page).to have_selector('.issue-boards-sidebar')
|
||||
end
|
||||
|
||||
it 'closes sidebar when clicking issue' do
|
||||
page.within(first('.board')) do
|
||||
first('.card').click
|
||||
end
|
||||
|
||||
expect(page).to have_selector('.issue-boards-sidebar')
|
||||
|
||||
page.within(first('.board')) do
|
||||
first('.card').click
|
||||
end
|
||||
|
||||
expect(page).not_to have_selector('.issue-boards-sidebar')
|
||||
end
|
||||
|
||||
it 'closes sidebar when clicking close button' do
|
||||
page.within(first('.board')) do
|
||||
first('.card').click
|
||||
end
|
||||
|
||||
expect(page).to have_selector('.issue-boards-sidebar')
|
||||
|
||||
find('.gutter-toggle').click
|
||||
|
||||
expect(page).not_to have_selector('.issue-boards-sidebar')
|
||||
end
|
||||
|
||||
it 'shows issue details when sidebar is open' do
|
||||
page.within(first('.board')) do
|
||||
first('.card').click
|
||||
end
|
||||
|
||||
page.within('.issue-boards-sidebar') do
|
||||
expect(page).to have_content(issue.title)
|
||||
expect(page).to have_content(issue.to_reference)
|
||||
end
|
||||
end
|
||||
|
||||
context 'assignee' do
|
||||
it 'updates the issues assignee' do
|
||||
page.within(first('.board')) do
|
||||
first('.card').click
|
||||
end
|
||||
|
||||
page.within('.assignee') do
|
||||
click_link 'Edit'
|
||||
|
||||
wait_for_ajax
|
||||
|
||||
page.within('.dropdown-menu-user') do
|
||||
click_link user.name
|
||||
|
||||
wait_for_vue_resource
|
||||
end
|
||||
|
||||
expect(page).to have_content(user.name)
|
||||
end
|
||||
|
||||
page.within(first('.board')) do
|
||||
page.within(first('.card')) do
|
||||
expect(page).to have_selector('.avatar')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'removes the assignee' do
|
||||
page.within(first('.board')) do
|
||||
find('.card:nth-child(2)').click
|
||||
end
|
||||
|
||||
page.within('.assignee') do
|
||||
click_link 'Edit'
|
||||
|
||||
wait_for_ajax
|
||||
|
||||
page.within('.dropdown-menu-user') do
|
||||
click_link 'Unassigned'
|
||||
|
||||
wait_for_vue_resource
|
||||
end
|
||||
|
||||
expect(page).to have_content('No assignee')
|
||||
end
|
||||
|
||||
page.within(first('.board')) do
|
||||
page.within(find('.card:nth-child(2)')) do
|
||||
expect(page).not_to have_selector('.avatar')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'assignees to current user' do
|
||||
page.within(first('.board')) do
|
||||
first('.card').click
|
||||
end
|
||||
|
||||
page.within('.assignee') do
|
||||
click_link 'assign yourself'
|
||||
|
||||
wait_for_vue_resource
|
||||
|
||||
expect(page).to have_content(user.name)
|
||||
end
|
||||
|
||||
page.within(first('.board')) do
|
||||
page.within(first('.card')) do
|
||||
expect(page).to have_selector('.avatar')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'milestone' do
|
||||
it 'adds a milestone' do
|
||||
page.within(first('.board')) do
|
||||
first('.card').click
|
||||
end
|
||||
|
||||
page.within('.milestone') do
|
||||
click_link 'Edit'
|
||||
|
||||
wait_for_ajax
|
||||
|
||||
click_link milestone.title
|
||||
|
||||
wait_for_vue_resource
|
||||
|
||||
page.within('.value') do
|
||||
expect(page).to have_content(milestone.title)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'removes a milestone' do
|
||||
page.within(first('.board')) do
|
||||
find('.card:nth-child(2)').click
|
||||
end
|
||||
|
||||
page.within('.milestone') do
|
||||
click_link 'Edit'
|
||||
|
||||
wait_for_ajax
|
||||
|
||||
click_link "No Milestone"
|
||||
|
||||
wait_for_vue_resource
|
||||
|
||||
page.within('.value') do
|
||||
expect(page).not_to have_content(milestone.title)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'due date' do
|
||||
it 'updates due date' do
|
||||
page.within(first('.board')) do
|
||||
first('.card').click
|
||||
end
|
||||
|
||||
page.within('.due_date') do
|
||||
click_link 'Edit'
|
||||
|
||||
click_link Date.today.day
|
||||
|
||||
wait_for_vue_resource
|
||||
|
||||
expect(page).to have_content(Date.today.to_s(:medium))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'labels' do
|
||||
it 'adds a single label' do
|
||||
page.within(first('.board')) do
|
||||
first('.card').click
|
||||
end
|
||||
|
||||
page.within('.labels') do
|
||||
click_link 'Edit'
|
||||
|
||||
wait_for_ajax
|
||||
|
||||
click_link label.title
|
||||
|
||||
wait_for_vue_resource
|
||||
|
||||
find('.dropdown-menu-close-icon').click
|
||||
|
||||
page.within('.value') do
|
||||
expect(page).to have_selector('.label', count: 1)
|
||||
expect(page).to have_content(label.title)
|
||||
end
|
||||
end
|
||||
|
||||
page.within(first('.board')) do
|
||||
page.within(first('.card')) do
|
||||
expect(page).to have_selector('.label', count: 1)
|
||||
expect(page).to have_content(label.title)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'adds a multiple labels' do
|
||||
page.within(first('.board')) do
|
||||
first('.card').click
|
||||
end
|
||||
|
||||
page.within('.labels') do
|
||||
click_link 'Edit'
|
||||
|
||||
wait_for_ajax
|
||||
|
||||
click_link label.title
|
||||
click_link label2.title
|
||||
|
||||
wait_for_vue_resource
|
||||
|
||||
find('.dropdown-menu-close-icon').click
|
||||
|
||||
page.within('.value') do
|
||||
expect(page).to have_selector('.label', count: 2)
|
||||
expect(page).to have_content(label.title)
|
||||
expect(page).to have_content(label2.title)
|
||||
end
|
||||
end
|
||||
|
||||
page.within(first('.board')) do
|
||||
page.within(first('.card')) do
|
||||
expect(page).to have_selector('.label', count: 2)
|
||||
expect(page).to have_content(label.title)
|
||||
expect(page).to have_content(label2.title)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'removes a label' do
|
||||
page.within(first('.board')) do
|
||||
find('.card:nth-child(2)').click
|
||||
end
|
||||
|
||||
page.within('.labels') do
|
||||
click_link 'Edit'
|
||||
|
||||
wait_for_ajax
|
||||
|
||||
click_link label.title
|
||||
|
||||
wait_for_vue_resource
|
||||
|
||||
find('.dropdown-menu-close-icon').click
|
||||
|
||||
page.within('.value') do
|
||||
expect(page).to have_selector('.label', count: 0)
|
||||
expect(page).not_to have_content(label.title)
|
||||
end
|
||||
end
|
||||
|
||||
page.within(first('.board')) do
|
||||
page.within(find('.card:nth-child(2)')) do
|
||||
expect(page).not_to have_selector('.label', count: 1)
|
||||
expect(page).not_to have_content(label.title)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'subscription' do
|
||||
it 'changes issue subscription' do
|
||||
page.within(first('.board')) do
|
||||
first('.card').click
|
||||
end
|
||||
|
||||
page.within('.subscription') do
|
||||
click_button 'Subscribe'
|
||||
|
||||
expect(page).to have_content("You're receiving notifications because you're subscribed to this thread.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -58,6 +58,22 @@ feature 'Issue filtering by Milestone', feature: true do
|
|||
expect(page).to have_css('.issue', count: 1)
|
||||
end
|
||||
|
||||
context 'when milestone has single quotes in title' do
|
||||
background do
|
||||
milestone.update(name: "rock 'n' roll")
|
||||
end
|
||||
|
||||
scenario 'filters by a specific Milestone', js: true do
|
||||
create(:issue, project: project, milestone: milestone)
|
||||
create(:issue, project: project)
|
||||
|
||||
visit_issues(project)
|
||||
filter_by_milestone(milestone.title)
|
||||
|
||||
expect(page).to have_css('.issue', count: 1)
|
||||
end
|
||||
end
|
||||
|
||||
def visit_issues(project)
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
end
|
||||
|
|
|
@ -40,8 +40,6 @@ feature 'Merge request created from fork' do
|
|||
end
|
||||
|
||||
context 'pipeline present in source project' do
|
||||
include WaitForAjax
|
||||
|
||||
given(:pipeline) do
|
||||
create(:ci_pipeline,
|
||||
project: fork_project,
|
||||
|
@ -57,7 +55,6 @@ feature 'Merge request created from fork' do
|
|||
scenario 'user visits a pipelines page', js: true do
|
||||
visit_merge_request(merge_request)
|
||||
page.within('.merge-request-tabs') { click_link 'Builds' }
|
||||
wait_for_ajax
|
||||
|
||||
page.within('table.ci-table') do
|
||||
expect(page).to have_content 'rspec'
|
||||
|
|
|
@ -67,6 +67,23 @@ feature 'Merge Request filtering by Milestone', feature: true do
|
|||
expect(page).to have_css('.merge-request', count: 1)
|
||||
end
|
||||
|
||||
context 'when milestone has single quotes in title' do
|
||||
background do
|
||||
milestone.update(name: "rock 'n' roll")
|
||||
end
|
||||
|
||||
scenario 'filters by a specific Milestone', js: true do
|
||||
create(:merge_request, :with_diffs, source_project: project, milestone: milestone)
|
||||
create(:merge_request, :simple, source_project: project)
|
||||
|
||||
visit_merge_requests(project)
|
||||
filter_by_milestone(milestone.title)
|
||||
|
||||
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
|
||||
expect(page).to have_css('.merge-request', count: 1)
|
||||
end
|
||||
end
|
||||
|
||||
def visit_merge_requests(project)
|
||||
visit namespace_project_merge_requests_path(project.namespace, project)
|
||||
end
|
||||
|
|
|
@ -77,7 +77,7 @@ feature 'issuable templates', feature: true, js: true do
|
|||
scenario 'user selects "bug" template' do
|
||||
select_template 'bug'
|
||||
wait_for_ajax
|
||||
preview_template("#{prior_description}\n\n#{template_content}")
|
||||
preview_template("#{template_content}")
|
||||
save_changes
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"iid": { "type": "integer" },
|
||||
"title": { "type": "string" },
|
||||
"confidential": { "type": "boolean" },
|
||||
"due_date": { "type": ["date", "null"] },
|
||||
"labels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
@ -42,7 +43,8 @@
|
|||
"name": { "type": "string" },
|
||||
"username": { "type": "string" },
|
||||
"avatar_url": { "type": "uri" }
|
||||
}
|
||||
},
|
||||
"subscribed": { "type": ["boolean", "null"] }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/*= require jquery.cookie.js */
|
||||
/*= require js.cookie.js */
|
||||
/*= require jquery.endless-scroll.js */
|
||||
/*= require pager */
|
||||
/*= require activities */
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
/*= require awards_handler */
|
||||
/*= require jquery */
|
||||
/*= require jquery.cookie */
|
||||
/*= require js.cookie */
|
||||
/*= require ./fixtures/emoji_menu */
|
||||
|
||||
(function() {
|
||||
|
@ -44,7 +44,6 @@
|
|||
spyOn(jQuery, 'get').and.callFake(function(req, cb) {
|
||||
return cb(window.emojiMenu);
|
||||
});
|
||||
spyOn(jQuery, 'cookie');
|
||||
});
|
||||
afterEach(function() {
|
||||
// restore original url root value
|
||||
|
@ -190,28 +189,6 @@
|
|||
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() {
|
||||
return it('should filter the emoji', function() {
|
||||
$('.js-add-award').eq(0).click();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//= require jquery
|
||||
//= require jquery_ujs
|
||||
//= require jquery.cookie
|
||||
//= require js.cookie
|
||||
//= require vue
|
||||
//= require vue-resource
|
||||
//= require lib/utils/url_utility
|
||||
|
@ -17,7 +17,7 @@
|
|||
gl.boardService = new BoardService('/test/issue-boards/board', '1');
|
||||
gl.issueBoards.BoardsStore.create();
|
||||
|
||||
$.cookie('issue_board_welcome_hidden', 'false');
|
||||
Cookies.set('issue_board_welcome_hidden', 'false');
|
||||
});
|
||||
|
||||
describe('Store', () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//= require jquery
|
||||
//= require jquery_ujs
|
||||
//= require jquery.cookie
|
||||
//= require js.cookie
|
||||
//= require vue
|
||||
//= require vue-resource
|
||||
//= require lib/utils/url_utility
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//= require jquery
|
||||
//= require jquery_ujs
|
||||
//= require jquery.cookie
|
||||
//= require js.cookie
|
||||
//= require vue
|
||||
//= require vue-resource
|
||||
//= require lib/utils/url_utility
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
/*= require right_sidebar */
|
||||
/*= require jquery */
|
||||
/*= require jquery.cookie */
|
||||
/*= require js.cookie */
|
||||
|
||||
(function() {
|
||||
var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState;
|
||||
|
|
|
@ -1,21 +1,36 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::ExclusiveLease do
|
||||
it 'cannot obtain twice before the lease has expired' do
|
||||
lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600)
|
||||
expect(lease.try_obtain).to eq(true)
|
||||
expect(lease.try_obtain).to eq(false)
|
||||
describe Gitlab::ExclusiveLease, type: :redis do
|
||||
let(:unique_key) { SecureRandom.hex(10) }
|
||||
|
||||
describe '#try_obtain' do
|
||||
it 'cannot obtain twice before the lease has expired' do
|
||||
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
|
||||
timeout = 1
|
||||
lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout)
|
||||
lease.try_obtain # start the lease
|
||||
sleep(2 * timeout) # lease should have expired now
|
||||
expect(lease.try_obtain).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
it 'can obtain after the lease has expired' do
|
||||
timeout = 1
|
||||
lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout)
|
||||
lease.try_obtain # start the lease
|
||||
sleep(2 * timeout) # lease should have expired now
|
||||
expect(lease.try_obtain).to eq(true)
|
||||
end
|
||||
describe '#exists?' do
|
||||
it 'returns true for an existing lease' do
|
||||
lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600)
|
||||
lease.try_obtain
|
||||
|
||||
def unique_key
|
||||
SecureRandom.hex(10)
|
||||
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
|
||||
|
|
|
@ -6,6 +6,7 @@ describe API::API, api: true do
|
|||
let(:user) { create(:user) }
|
||||
let(:admin) { create(:admin) }
|
||||
let(:project) { create(:project, creator_id: user.id) }
|
||||
let(:project2) { create(:project, creator_id: user.id) }
|
||||
let(:deploy_key) { create(:deploy_key, public: true) }
|
||||
|
||||
let!(:deploy_keys_project) do
|
||||
|
@ -96,6 +97,22 @@ describe API::API, api: true do
|
|||
post api("/projects/#{project.id}/deploy_keys", admin), key_attrs
|
||||
end.to change{ project.deploy_keys.count }.by(1)
|
||||
end
|
||||
|
||||
it 'returns an existing ssh key when attempting to add a duplicate' do
|
||||
expect do
|
||||
post api("/projects/#{project.id}/deploy_keys", admin), { key: deploy_key.key, title: deploy_key.title }
|
||||
end.not_to change { project.deploy_keys.count }
|
||||
|
||||
expect(response).to have_http_status(201)
|
||||
end
|
||||
|
||||
it 'joins an existing ssh key to a new project' do
|
||||
expect do
|
||||
post api("/projects/#{project2.id}/deploy_keys", admin), { key: deploy_key.key, title: deploy_key.title }
|
||||
end.to change { project2.deploy_keys.count }.by(1)
|
||||
|
||||
expect(response).to have_http_status(201)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /projects/:id/deploy_keys/:key_id' do
|
||||
|
|
|
@ -958,6 +958,29 @@ describe API::API, api: true do
|
|||
expect(joined_event['author']['name']).to eq(user.name)
|
||||
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
|
||||
|
||||
it 'returns a 404 error if not found' do
|
||||
|
|
|
@ -50,6 +50,12 @@ RSpec.configure do |config|
|
|||
example.run
|
||||
Rails.cache = caching_store
|
||||
end
|
||||
|
||||
config.around(:each, :redis) do |example|
|
||||
Gitlab::Redis.with(&:flushall)
|
||||
example.run
|
||||
Gitlab::Redis.with(&:flushall)
|
||||
end
|
||||
end
|
||||
|
||||
FactoryGirl::SyntaxRunner.class_eval do
|
||||
|
|
|
@ -5,6 +5,26 @@ describe ProjectCacheWorker do
|
|||
|
||||
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
|
||||
context 'when an exclusive lease can be obtained' do
|
||||
before do
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
/**
|
||||
* 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;
|
||||
};
|
|
@ -0,0 +1,156 @@
|
|||
/*!
|
||||
* 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 () {});
|
||||
}));
|
Loading…
Reference in New Issue