Commit 79440890 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'master' into copy-as-md

parents bd2880bb 8c9a06c3
...@@ -35,7 +35,6 @@ stages: ...@@ -35,7 +35,6 @@ stages:
.dedicated-runner: &dedicated-runner .dedicated-runner: &dedicated-runner
tags: tags:
- gitlab-org - gitlab-org
- 2gb
.knapsack-state: &knapsack-state .knapsack-state: &knapsack-state
services: [] services: []
......
...@@ -2,6 +2,136 @@ ...@@ -2,6 +2,136 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 8.16.0 (2017-02-22)
- Add LDAP Rake task to rename a provider. !2181
- Validate label's title length. !5767 (Tomáš Kukrál)
- Allow to add deploy keys with write-access. !5807 (Ali Ibrahim)
- Allow to use + symbol in filenames. !6644 (blackst0ne)
- Search bar redesign first iteration. !7345
- Fix date inconsistency on due date picker. !7422 (Giuliano Varriale)
- Add email confirmation field to registration form. !7432
- Updated project visibility settings UX. !7645
- Go to a project order. !7737 (Jacopo Beschi @jacopo-beschi)
- Support slash comand `/merge` for merging merge requests. !7746 (Jarka Kadlecova)
- Add more storage statistics. !7754 (Markus Koller)
- Add support for PlantUML diagrams in AsciiDoc documents. !7810 (Horacio Sanson)
- Remove extra orphaned rows when removing stray namespaces. !7841
- Added lighter count badge background-color for on white backgrounds. !7873
- Fixes issue boards list colored top border visual glitch. !7898 (Pier Paolo Ramon)
- change 'gray' color theme name to 'black' to match the actual color. !7908 (BM5k)
- Remove trailing whitespace when generating changelog entry. !7948
- Remove checking branches state in issue new branch button. !8023
- Log LDAP blocking/unblocking events to application log. !8042 (Markus Koller)
- ensure permalinks scroll to correct position on multiple clicks. !8046
- Allow to use ENV variables in redis config. !8073 (Semyon Pupkov)
- fix button layout issue on branches page. !8074
- Reduce DB-load for build-queues by storing last_update in Redis. !8084
- Record and show last used date of SSH Keys. !8113 (Vincent Wong)
- Resolves overflow in compare branch and tags dropdown. !8118
- Replace wording for slash command confirmation message. !8123
- remove build_user. !8162 (Arsenev Vladislav)
- Prevent empty pagination when list is not empty. !8172
- Make successful pipeline emails off for watchers. !8176
- Improve copy in Issue Tracker empty state. !8202
- Adds CSS class to status icon on MR widget to prevent non-colored icon. !8219
- Improve visibility of "Resolve conflicts" and "Merge locally" actions. !8229
- Add Gitaly to the architecture documentation. !8264 (Pablo Carranza <pablo@gitlab.com>)
- Sort numbers in build names more intelligently. !8277
- Show nested groups tab on group page. !8308
- Rename users with namespace ending with .git. !8309
- Rename filename to file path in tooltip of file header in merge request diff. !8314
- About GitLab link in sidebar that links to help page. !8316
- Merged the 'Groups' and 'Projects' tabs when viewing user profiles. !8323 (James Gregory)
- re-enable change username button after failure. !8332
- Darkened hr border color in descriptions because of update of bootstrap. !8333
- display merge request discussion tab for empty branches. !8347
- Fix double spaced CI log. !8349 (Jared Deckard <jared.deckard@gmail.com>)
- Refactored note edit form to improve frontend performance on MR and Issues pages, especially pages with has a lot of discussions in it. !8356
- Make CTRL+Enter submits a new merge request. !8360 (Saad Shahd)
- Fixes too short input for placeholder message in commit listing page. !8367
- Fix typo: seach to search. !8370
- Adds label to Environments "Date Created". !8376 (Saad Shahd)
- Convert project setting text into protected branch path link. !8377 (Ken Ding)
- Precompile all JavaScript fixtures. !8384
- Use original casing for build action text. !8387
- Scroll to bottom on build completion if autoscroll was active. !8391
- Properly handle failed reCAPTCHA on user registration. !8403
- Changed alerts to be responsive, centered text on smaller viewports. !8424 (Connor Smallman)
- Pass Gitaly resource path to gitlab-workhorse if Gitaly is enabled. !8440
- Fixes and Improves CSS and HTML problems in mini pipeline graph and builds dropdown. !8443
- Don't instrument 405 Grape calls. !8445
- Change CI template linter textarea with Ace Editor. !8452 (Didem Acet)
- Removes unneeded `window` declaration in environments related code. !8456
- API: fix query response for `/projects/:id/issues?milestone="No%20Milestone"`. !8457 (Panagiotis Atmatzidis, David Eisner)
- Fix broken url on group avatar. !8464 (hogewest)
- Fixes buttons not being accessible via the keyboard when creating new group. !8469
- Restore backup correctly when "BACKUP" environment variable is passed. !8477
- Add new endpoints for Time Tracking. !8483
- Fix Compare page throws 500 error when any branch/reference is not selected. !8492 (Martin Cabrera)
- Treat environments matching `production/*` as Production. !8500
- Hide build artifacts keep button if operation is not allowed. !8501
- Update the gitlab-markup gem to the version 1.5.1. !8509
- Remove Lock Icon on Protected Tag. !8513 (Sergey Nikitin)
- Use cached values to compute total issues count in milestone index pages. !8518
- Speed up dashboard milestone index by scoping IssuesFinder to user authorized projects. !8524
- Copy <some text> to clipboard. !8535
- Check for env[Grape::Env::GRAPE_ROUTING_ARGS] instead of endpoint.route. !8544
- Fixes builds dropdown making request when clicked to be closed. !8545
- Fixes pipeline status cell is too wide by adding missing classes in table head cells. !8549
- Mutate the attribute instead of issuing a write operation to the DB in `ProjectFeaturesCompatibility` concern. !8552
- Fix links to commits pages on pipelines list page. !8558
- Ensure updating project settings shows a flash message on success. !8579 (Sandish Chen)
- Fixes big pipeline and small pipeline width problems and tooltips text being outside the tooltip. !8593
- Autoresize markdown preview. !8607 (Didem Acet)
- Link external build badge to its target URL. !8611
- Adjust ProjectStatistic#repository_size with values saved as MB. !8616
- Correct User-agent placement in robots.txt. !8623 (Eric Sabelhaus)
- Record used SSH keys only once per day. !8655
- Do not generate pipeline branch/tag path if not present. !8658
- Fix Merge When Pipeline Succeeds immediate merge bug. !8685
- Fix blame 500 error on invalid path. !25761 (Jeff Stubler)
- Added animations to issue boards interactions.
- Check if user can read project before being assigned to issue.
- Show 'too many changes' message for created merge requests when they are too large.
- Fix redirect after update file when user has forked project.
- Parse JIRA issue references even if Issue Tracker is disabled.
- Made download artifacts button accessible via keyboard by changing it from an anchor tag to an actual button. (Ryan Harris)
- Make play button on Pipelines page accessible via keyboard. (Ryan Harris)
- Decreases font-size on login page.
- Fixed merge request tabs dont move when opening collapsed sidebar.
- Display project avatars on Admin Area and Projects pages for mobile views. (Ryan Harris)
- Fix participants margins to fit on one line.
- 26352 Change Profile settings to User / Settings.
- Fix Commits API to accept a Project path upon POST.
- Expire related caches after changing HEAD. (Minqi Pan)
- Add various hover animations throughout the application.
- Re-order update steps in the 8.14 -> 8.15 upgrade guide.
- Move award emoji's out of the discussion tab for merge requests.
- Synchronize all project authorization refreshing work to prevent race conditions.
- Remove the project_authorizations.id column.
- Combined the settings options project members and groups into a single one called members.
- Change earlier to task_status_short to avoid titlebar line wraps.
- 25701 standardize text colors.
- Handle HTTP errors in environment list.
- Re-add Google Cloud Storage as a backup strategy.
- Change status colors of runners to better defaults.
- Added number_with_delimiter to counter on milestone panels. (Ryan Harris)
- Query external CI statuses in the background.
- Allow group and project paths when transferring projects via the API.
- Don't validate environment urls on .gitlab-ci.yml.
- Fix a Grape deprecation, use `#request_method` instead of `#route_method`.
- Fill missing authorized projects rows.
- Allow API query to find projects with dots in their name. (Bruno Melli)
- Fix import/export wrong user mapping.
- Removed bottom padding from merge manually from CLI because of repositioning award emoji's.
- Fix project queued for deletion re-creation tooltip.
- Fix search group/project filtering to show results.
- Fix 500 error when POSTing to Users API with optional confirm param.
- 26504 Fix styling of MR jump to discussion button.
- Add margin to markdown math blocks.
- Add hover state to MR comment reply button.
## 8.15.4 (2017-01-09) ## 8.15.4 (2017-01-09)
- Make successful pipeline emails off for watchers. !8176 - Make successful pipeline emails off for watchers. !8176
......
...@@ -21,7 +21,7 @@ gem 'rugged', '~> 0.24.0' ...@@ -21,7 +21,7 @@ gem 'rugged', '~> 0.24.0'
# Authentication libraries # Authentication libraries
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0' gem 'doorkeeper', '~> 4.2.0'
gem 'omniauth', '~> 1.3.1' gem 'omniauth', '~> 1.3.2'
gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6' gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-cas3', '~> 1.1.2' gem 'omniauth-cas3', '~> 1.1.2'
......
...@@ -449,7 +449,7 @@ GEM ...@@ -449,7 +449,7 @@ GEM
octokit (4.6.2) octokit (4.6.2)
sawyer (~> 0.8.0, >= 0.5.3) sawyer (~> 0.8.0, >= 0.5.3)
oj (2.17.4) oj (2.17.4)
omniauth (1.3.1) omniauth (1.3.2)
hashie (>= 1.2, < 4) hashie (>= 1.2, < 4)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
omniauth-auth0 (1.4.1) omniauth-auth0 (1.4.1)
...@@ -925,7 +925,7 @@ DEPENDENCIES ...@@ -925,7 +925,7 @@ DEPENDENCIES
oauth2 (~> 1.2.0) oauth2 (~> 1.2.0)
octokit (~> 4.6.2) octokit (~> 4.6.2)
oj (~> 2.17.4) oj (~> 2.17.4)
omniauth (~> 1.3.1) omniauth (~> 1.3.2)
omniauth-auth0 (~> 1.4.1) omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.2.0) omniauth-authentiq (~> 0.2.0)
omniauth-azure-oauth2 (~> 0.0.6) omniauth-azure-oauth2 (~> 0.0.6)
......
...@@ -113,4 +113,4 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on ...@@ -113,4 +113,4 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on
## Is it awesome? ## Is it awesome?
Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua. Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua.
[These people](https://twitter.com/gitlab/favorites) seem to like it. [These people](https://twitter.com/gitlab/likes) seem to like it.
8.16.0-pre 8.17.0-pre
...@@ -62,6 +62,7 @@ var DropDown = function(list) { ...@@ -62,6 +62,7 @@ var DropDown = function(list) {
this.list = list; this.list = list;
this.items = []; this.items = [];
this.getItems(); this.getItems();
this.initTemplateString();
this.addEvents(); this.addEvents();
this.initialState = list.innerHTML; this.initialState = list.innerHTML;
}; };
...@@ -72,6 +73,17 @@ Object.assign(DropDown.prototype, { ...@@ -72,6 +73,17 @@ Object.assign(DropDown.prototype, {
return this.items; return this.items;
}, },
initTemplateString: function() {
var items = this.items || this.getItems();
var templateString = '';
if(items.length > 0) {
templateString = items[items.length - 1].outerHTML;
}
this.templateString = templateString;
return this.templateString;
},
clickEvent: function(e) { clickEvent: function(e) {
// climb up the tree to find the LI // climb up the tree to find the LI
var selected = utils.closest(e.target, 'LI'); var selected = utils.closest(e.target, 'LI');
...@@ -111,30 +123,21 @@ Object.assign(DropDown.prototype, { ...@@ -111,30 +123,21 @@ Object.assign(DropDown.prototype, {
addData: function(data) { addData: function(data) {
this.data = (this.data || []).concat(data); this.data = (this.data || []).concat(data);
this.render(data); this.render(this.data);
}, },
// call render manually on data; // call render manually on data;
render: function(data){ render: function(data){
// debugger // debugger
// empty the list first // empty the list first
var sampleItem; var templateString = this.templateString;
var newChildren = []; var newChildren = [];
var toAppend; var toAppend;
for(var i = 0; i < this.items.length; i++) { newChildren = (data ||[]).map(function(dat){
var item = this.items[i]; var html = utils.t(templateString, dat);
sampleItem = item;
if(item.parentNode && item.parentNode.dataset.hasOwnProperty('dynamic')) {
item.parentNode.removeChild(item);
}
}
newChildren = this.data.map(function(dat){
var html = utils.t(sampleItem.outerHTML, dat);
var template = document.createElement('div'); var template = document.createElement('div');
template.innerHTML = html; template.innerHTML = html;
// console.log(template.content)
// Help set the image src template // Help set the image src template
var imageTags = template.querySelectorAll('img[data-src]'); var imageTags = template.querySelectorAll('img[data-src]');
...@@ -156,7 +159,7 @@ Object.assign(DropDown.prototype, { ...@@ -156,7 +159,7 @@ Object.assign(DropDown.prototype, {
if(toAppend) { if(toAppend) {
toAppend.innerHTML = newChildren.join(''); toAppend.innerHTML = newChildren.join('');
} else { } else {
this.list.innerHTML = newChildren.join(''); this.list.innerHTML = newChildren.join('');
} }
}, },
...@@ -173,10 +176,7 @@ Object.assign(DropDown.prototype, { ...@@ -173,10 +176,7 @@ Object.assign(DropDown.prototype, {
}, },
destroy: function() { destroy: function() {
if (!this.hidden) { this.hide();
this.hide();
}
this.list.removeEventListener('click', this.clickWrapper); this.list.removeEventListener('click', this.clickWrapper);
} }
}); });
...@@ -278,7 +278,7 @@ require('./window')(function(w){ ...@@ -278,7 +278,7 @@ require('./window')(function(w){
self.hooks[i].list.hide(); self.hooks[i].list.hide();
} }
}.bind(this); }.bind(this);
w.addEventListener('click', this.windowClickedWrapper); document.addEventListener('click', this.windowClickedWrapper);
}, },
removeEvents: function(){ removeEvents: function(){
...@@ -307,7 +307,7 @@ require('./window')(function(w){ ...@@ -307,7 +307,7 @@ require('./window')(function(w){
if(!list){ if(!list){
list = document.querySelector(hook.dataset[utils.toDataCamelCase(DATA_TRIGGER)]); list = document.querySelector(hook.dataset[utils.toDataCamelCase(DATA_TRIGGER)]);
} }
if(hook) { if(hook) {
if(hook.tagName === 'A' || hook.tagName === 'BUTTON') { if(hook.tagName === 'A' || hook.tagName === 'BUTTON') {
this.hooks.push(new HookButton(hook, list, plugins, config)); this.hooks.push(new HookButton(hook, list, plugins, config));
...@@ -462,6 +462,8 @@ Object.assign(HookInput.prototype, { ...@@ -462,6 +462,8 @@ Object.assign(HookInput.prototype, {
var self = this; var self = this;
this.mousedown = function mousedown(e) { this.mousedown = function mousedown(e) {
if(self.hasRemovedEvents) return;
var mouseEvent = new CustomEvent('mousedown.dl', { var mouseEvent = new CustomEvent('mousedown.dl', {
detail: { detail: {
hook: self, hook: self,
...@@ -474,6 +476,8 @@ Object.assign(HookInput.prototype, { ...@@ -474,6 +476,8 @@ Object.assign(HookInput.prototype, {
} }
this.input = function input(e) { this.input = function input(e) {
if(self.hasRemovedEvents) return;
var inputEvent = new CustomEvent('input.dl', { var inputEvent = new CustomEvent('input.dl', {
detail: { detail: {
hook: self, hook: self,
...@@ -487,10 +491,14 @@ Object.assign(HookInput.prototype, { ...@@ -487,10 +491,14 @@ Object.assign(HookInput.prototype, {
} }
this.keyup = function keyup(e) { this.keyup = function keyup(e) {
if(self.hasRemovedEvents) return;
keyEvent(e, 'keyup.dl'); keyEvent(e, 'keyup.dl');
} }
this.keydown = function keydown(e) { this.keydown = function keydown(e) {
if(self.hasRemovedEvents) return;
keyEvent(e, 'keydown.dl'); keyEvent(e, 'keydown.dl');
} }
...@@ -520,7 +528,8 @@ Object.assign(HookInput.prototype, { ...@@ -520,7 +528,8 @@ Object.assign(HookInput.prototype, {
this.trigger.addEventListener('keydown', this.keydown); this.trigger.addEventListener('keydown', this.keydown);
}, },
removeEvents: function(){ removeEvents: function() {
this.hasRemovedEvents = true;
this.trigger.removeEventListener('mousedown', this.mousedown); this.trigger.removeEventListener('mousedown', this.mousedown);
this.trigger.removeEventListener('input', this.input); this.trigger.removeEventListener('input', this.input);
this.trigger.removeEventListener('keyup', this.keyup); this.trigger.removeEventListener('keyup', this.keyup);
...@@ -578,7 +587,7 @@ require('./window')(function(w){ ...@@ -578,7 +587,7 @@ require('./window')(function(w){
var listItems = removeHighlight(list); var listItems = removeHighlight(list);
if(currentIndex>0){ if(currentIndex>0){
if(!listItems[currentIndex-1]){ if(!listItems[currentIndex-1]){
currentIndex = currentIndex-1; currentIndex = currentIndex-1;
} }
listItems[currentIndex-1].classList.add('dropdown-active'); listItems[currentIndex-1].classList.add('dropdown-active');
} }
...@@ -630,7 +639,7 @@ require('./window')(function(w){ ...@@ -630,7 +639,7 @@ require('./window')(function(w){
return; return;
} }
if(currentKey === 'ArrowUp') { if(currentKey === 'ArrowUp') {
isUpArrow = true; isUpArrow = true;
} }
if(currentKey === 'ArrowDown') { if(currentKey === 'ArrowDown') {
isDownArrow = true; isDownArrow = true;
...@@ -668,16 +677,16 @@ var camelize = function(str) { ...@@ -668,16 +677,16 @@ var camelize = function(str) {
}; };
var closest = function(thisTag, stopTag) { var closest = function(thisTag, stopTag) {
while(thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){ while(thisTag && thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){
thisTag = thisTag.parentNode; thisTag = thisTag.parentNode;
} }
return thisTag; return thisTag;
}; };
var isDropDownParts = function(target) { var isDropDownParts = function(target) {
if(target.tagName === 'HTML') { return false; } if(!target || target.tagName === 'HTML') { return false; }
return ( return (
target.hasAttribute(DATA_TRIGGER) || target.hasAttribute(DATA_TRIGGER) ||
target.hasAttribute(DATA_DROPDOWN) target.hasAttribute(DATA_DROPDOWN)
); );
}; };
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign, no-new */
/* global Vue */ /* global Vue */
/* global EnvironmentsService */ /* global EnvironmentsService */
/* global Flash */
//= require vue //= require vue
//= require vue-resource //= require vue-resource
...@@ -10,41 +11,6 @@ ...@@ -10,41 +11,6 @@
(() => { (() => {
window.gl = window.gl || {}; window.gl = window.gl || {};
/**
* Given the visibility prop provided by the url query parameter and which
* changes according to the active tab we need to filter which environments
* should be visible.
*
* The environments array is a recursive tree structure and we need to filter
* both root level environments and children environments.
*
* In order to acomplish that, both `filterState` and `filterEnvironmentsByState`
* functions work together.
* The first one works as the filter that verifies if the given environment matches
* the given state.
* The second guarantees both root level and children elements are filtered as well.
*/
const filterState = state => environment => environment.state === state && environment;
/**
* Given the filter function and the array of environments will return only
* the environments that match the state provided to the filter function.
*
* @param {Function} fn
* @param {Array} array
* @return {Array}
*/
const filterEnvironmentsByState = (fn, arr) => arr.map((item) => {
if (item.children) {
const filteredChildren = filterEnvironmentsByState(fn, item.children).filter(Boolean);
if (filteredChildren.length) {
item.children = filteredChildren;
return item;
}
}
return fn(item);
}).filter(Boolean);
gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', { gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', {
props: { props: {
store: { store: {
...@@ -81,10 +47,6 @@ ...@@ -81,10 +47,6 @@
}, },
computed: { computed: {
filteredEnvironments() {
return filterEnvironmentsByState(filterState(this.visibility), this.state.environments);
},
scope() { scope() {
return this.$options.getQueryParameter('scope'); return this.$options.getQueryParameter('scope');
}, },
...@@ -111,7 +73,7 @@ ...@@ -111,7 +73,7 @@
const scope = this.$options.getQueryParameter('scope'); const scope = this.$options.getQueryParameter('scope');
if (scope) { if (scope) {
this.visibility = scope; this.store.storeVisibility(scope);
} }
this.isLoading = true; this.isLoading = true;
...@@ -121,6 +83,10 @@ ...@@ -121,6 +83,10 @@
.then((json) => { .then((json) => {
this.store.storeEnvironments(json); this.store.storeEnvironments(json);
this.isLoading = false; this.isLoading = false;
})
.catch(() => {
this.isLoading = false;
new Flash('An error occurred while fetching the environments.', 'alert');
}); });
}, },
...@@ -188,7 +154,7 @@ ...@@ -188,7 +154,7 @@
<div class="blank-state blank-state-no-icon" <div class="blank-state blank-state-no-icon"
v-if="!isLoading && state.environments.length === 0"> v-if="!isLoading && state.environments.length === 0">
<h2 class="blank-state-title"> <h2 class="blank-state-title js-blank-state-title">
You don't have any environments right now. You don't have any environments right now.
</h2> </h2>
<p class="blank-state-text"> <p class="blank-state-text">
...@@ -202,13 +168,13 @@ ...@@ -202,13 +168,13 @@
<a <a
v-if="canCreateEnvironmentParsed" v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath" :href="newEnvironmentPath"
class="btn btn-create"> class="btn btn-create js-new-environment-button">
New Environment New Environment
</a> </a>
</div> </div>
<div class="table-holder" <div class="table-holder"
v-if="!isLoading && state.environments.length > 0"> v-if="!isLoading && state.filteredEnvironments.length > 0">
<table class="table ci-table environments"> <table class="table ci-table environments">
<thead> <thead>
<tr> <tr>
...@@ -221,7 +187,7 @@ ...@@ -221,7 +187,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<template v-for="model in filteredEnvironments" <template v-for="model in state.filteredEnvironments"
v-bind:model="model"> v-bind:model="model">
<tr <tr
......
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
this.state.environments = []; this.state.environments = [];
this.state.stoppedCounter = 0; this.state.stoppedCounter = 0;
this.state.availableCounter = 0; this.state.availableCounter = 0;
this.state.visibility = 'available';
this.state.filteredEnvironments = [];
return this; return this;
}, },
...@@ -59,7 +61,7 @@ ...@@ -59,7 +61,7 @@
if (occurs.length) { if (occurs.length) {
acc[acc.indexOf(occurs[0])].children.push(environment); acc[acc.indexOf(occurs[0])].children.push(environment);
acc[acc.indexOf(occurs[0])].children.sort(this.sortByName); acc[acc.indexOf(occurs[0])].children.slice().sort(this.sortByName);
} else { } else {
acc.push({ acc.push({
name: environment.environment_type, name: environment.environment_type,
...@@ -73,13 +75,70 @@ ...@@ -73,13 +75,70 @@
} }
return acc; return acc;
}, []).sort(this.sortByName); }, []).slice().sort(this.sortByName);
this.state.environments = environmentsTree; this.state.environments = environmentsTree;
this.filterEnvironmentsByVisibility(this.state.environments);
return environmentsTree; return environmentsTree;
}, },
storeVisibility(visibility) {
this.state.visibility = visibility;
},
/**
* Given the visibility prop provided by the url query parameter and which
* changes according to the active tab we need to filter which environments
* should be visible.
*
* The environments array is a recursive tree structure and we need to filter
* both root level environments and children environments.
*
* In order to acomplish that, both `filterState` and `filterEnvironmentsByVisibility`
* functions work together.
* The first one works as the filter that verifies if the given environment matches
* the given state.
* The second guarantees both root level and children elements are filtered as well.
*
* Given array of environments will return only
* the environments that match the state stored.
*
* @param {Array} array
* @return {Array}
*/
filterEnvironmentsByVisibility(arr) {
const filteredEnvironments = arr.map((item) => {
if (item.children) {
const filteredChildren = this.filterEnvironmentsByVisibility(
item.children,
).filter(Boolean);
if (filteredChildren.length) {
item.children = filteredChildren;
return item;
}
}
return this.filterState(this.state.visibility, item);
}).filter(Boolean);
this.state.filteredEnvironments = filteredEnvironments;
return filteredEnvironments;
},
/**
* Given the state and the environment,
* returns only if the environment state matches the one provided.
*
* @param {String} state
* @param {Object} environment
* @return {Object}
*/
filterState(state, environment) {
return environment.state === state && environment;
},
/** /**
* Toggles folder open property given the environment type. * Toggles folder open property given the environment type.
* *
......
/* global CustomEvent */
/* eslint-disable no-global-assign */
// Custom event support for IE
CustomEvent = function CustomEvent(event, parameters) {
const params = parameters || { bubbles: false, cancelable: false, detail: undefined };
const evt = document.createEvent('CustomEvent');
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
return evt;
};
CustomEvent.prototype = window.Event.prototype;
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
this.config = { this.config = {
droplabFilter: { droplabFilter: {
template: 'hint', template: 'hint',
filterFunction: gl.DropdownUtils.filterHint, filterFunction: gl.DropdownUtils.filterHint.bind(null, input),
}, },
}; };
} }
...@@ -20,6 +20,9 @@ ...@@ -20,6 +20,9 @@
if (selected.tagName === 'LI') { if (selected.tagName === 'LI') {
if (selected.hasAttribute('data-value')) { if (selected.hasAttribute('data-value')) {
this.dismissDropdown(); this.dismissDropdown();
} else if (selected.getAttribute('data-action') === 'submit') {
this.dismissDropdown();
this.dispatchFormSubmitEvent();
} else { } else {
const token = selected.querySelector('.js-filter-hint').innerText.trim(); const token = selected.querySelector('.js-filter-hint').innerText.trim();
const tag = selected.querySelector('.js-filter-tag').innerText.trim(); const tag = selected.querySelector('.js-filter-tag').innerText.trim();
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
loadingTemplate: this.loadingTemplate, loadingTemplate: this.loadingTemplate,
}, },
droplabFilter: { droplabFilter: {
filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol), filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input),
}, },
}; };
} }
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
} }
getSearchInput() { getSearchInput() {
const query = this.input.value.trim(); const query = gl.DropdownUtils.getSearchInput(this.input);
const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
return lastToken.value || ''; return lastToken.value || '';
......
...@@ -20,17 +20,15 @@ ...@@ -20,17 +20,15 @@
return escapedText; return escapedText;
} }
static filterWithSymbol(filterSymbol, item, query) { static filterWithSymbol(filterSymbol, input, item) {
const updatedItem = item; const updatedItem = item;
const query = gl.DropdownUtils.getSearchInput(input);
const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query); const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query);
if (lastToken !== searchToken) { if (lastToken !== searchToken) {
const title = updatedItem.title.toLowerCase(); const title = updatedItem.title.toLowerCase();
let value = lastToken.value.toLowerCase(); let value = lastToken.value.toLowerCase();
value = value.replace(/"(.*?)"/g, str => str.slice(1).slice(0, -1));
if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
value = value.slice(1);
}
// Eg. filterSymbol = ~ for labels // Eg. filterSymbol = ~ for labels
const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1; const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1;
...@@ -44,8 +42,9 @@ ...@@ -44,8 +42,9 @@
return updatedItem; return updatedItem;
} }
static filterHint(item, query) { static filterHint(input, item) {
const updatedItem = item; const updatedItem = item;
const query = gl.DropdownUtils.getSearchInput(input);
let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
lastToken = lastToken.key || lastToken || ''; lastToken = lastToken.key || lastToken || '';
...@@ -72,6 +71,48 @@ ...@@ -72,6 +71,48 @@
// Return boolean based on whether it was set // Return boolean based on whether it was set
return dataValue !== null; return dataValue !== null;
} }
static getSearchInput(filteredSearchInput) {
const inputValue = filteredSearchInput.value;
const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
return inputValue.slice(0, right);
}
static getInputSelectionPosition(input) {
const selectionStart = input.selectionStart;
let inputValue = input.value;
// Replace all spaces inside quote marks with underscores
// This helps with matching the beginning & end of a token:key
inputValue = inputValue.replace(/"(.*?)"/g, str => str.replace(/\s/g, '_'));
// Get the right position for the word selected
// Regex matches first space
let right = inputValue.slice(selectionStart).search(/\s/);
if (right >= 0) {
right += selectionStart;
} else if (right < 0) {
right = inputValue.length;
}
// Get the left position for the word selected
// Regex matches last non-whitespace character
let left = inputValue.slice(0, right).search(/\S+$/);
if (selectionStart === 0) {
left = 0;
} else if (selectionStart === inputValue.length && left < 0) {
left = inputValue.length;
} else if (left < 0) {
left = selectionStart;
}
return {
left,
right,
};
}
} }
window.gl = window.gl || {}; window.gl = window.gl || {};
......
...@@ -39,6 +39,7 @@ ...@@ -39,6 +39,7 @@
} }
this.dismissDropdown(); this.dismissDropdown();
this.dispatchInputEvent();
} }
} }
...@@ -78,7 +79,16 @@ ...@@ -78,7 +79,16 @@
dispatchInputEvent() { dispatchInputEvent() {
// Propogate input change to FilteredSearchDropdownManager // Propogate input change to FilteredSearchDropdownManager
// so that it can determine which dropdowns to open // so that it can determine which dropdowns to open
this.input.dispatchEvent(new Event('input')); this.input.dispatchEvent(new CustomEvent('input', {
bubbles: true,
cancelable: true,
}));
}
dispatchFormSubmitEvent() {
// dispatchEvent() is necessary as form.submit() does not
// trigger event handlers
this.input.form.dispatchEvent(new Event('submit'));
} }
hideDropdown() { hideDropdown() {
......
...@@ -57,28 +57,33 @@ ...@@ -57,28 +57,33 @@
static addWordToInput(tokenName, tokenValue = '') { static addWordToInput(tokenName, tokenValue = '') {
const input = document.querySelector('.filtered-search'); const input = document.querySelector('.filtered-search');
const inputValue = input.value;
const word = `${tokenName}:${tokenValue}`; const word = `${tokenName}:${tokenValue}`;
const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(input.value); // Get the string to replace
const lastSearchToken = searchToken.split(' ').last(); let newCaretPosition = input.selectionStart;
const lastInputCharacter = input.value[input.value.length - 1]; const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input);
const lastInputTrimmedCharacter = input.value.trim()[input.value.trim().length - 1];
input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`;
// Remove the typed tokenName
if (word.indexOf(lastSearchToken) === 0 && searchToken !== '') { // If we have added a tokenValue at the end of the input,
// Remove spaces after the colon // add a space and set selection to the end
if (lastInputCharacter === ' ' && lastInputTrimmedCharacter === ':') { if (right >= inputValue.length && tokenValue !== '') {
input.value = input.value.trim(); input.value += ' ';
} newCaretPosition = input.value.length;
input.value = input.value.slice(0, -1 * lastSearchToken.length);
} else if (lastInputCharacter !== ' ' || (lastToken && lastToken.value[lastToken.value.length - 1] === ' ')) {
// Remove the existing tokenValue
const lastTokenString = `${lastToken.key}:${lastToken.symbol}${lastToken.value}`;
input.value = input.value.slice(0, -1 * lastTokenString.length);
} }
input.value += word; gl.FilteredSearchDropdownManager.updateInputCaretPosition(newCaretPosition, input);
}
static updateInputCaretPosition(selectionStart, input) {
// Reset the position
// Sometimes can end up at end of input
input.setSelectionRange(selectionStart, selectionStart);
const { right } = gl.DropdownUtils.getInputSelectionPosition(input);
input.setSelectionRange(right, right);
} }
updateCurrentDropdownOffset() { updateCurrentDropdownOffset() {
...@@ -90,9 +95,18 @@ ...@@ -90,9 +95,18 @@
this.font = window.getComputedStyle(this.filteredSearchInput).font; this.font = window.getComputedStyle(this.filteredSearchInput).font;
} }
const input = this.filteredSearchInput;
const inputText = input.value.slice(0, input.selectionStart);
const filterIconPadding = 27; const filterIconPadding = 27;
const offset = gl.text let offset = gl.text.getTextWidth(inputText, this.font) + filterIconPadding;
.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding;
const currentDropdownWidth = this.mapping[key].element.clientWidth === 0 ? 200 :
this.mapping[key].element.clientWidth;
const offsetMaxWidth = this.filteredSearchInput.clientWidth - currentDropdownWidth;
if (offsetMaxWidth < offset) {
offset = offsetMaxWidth;
}
this.mapping[key].reference.setOffset(offset); this.mapping[key].reference.setOffset(offset);
} }
...@@ -148,9 +162,9 @@ ...@@ -148,9 +162,9 @@
setDropdown() { setDropdown() {
const { lastToken, searchToken } = this.tokenizer const { lastToken, searchToken } = this.tokenizer
.processTokens(this.filteredSearchInput.value); .processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput));
if (this.filteredSearchInput.value.split('').last() === ' ') { if (this.currentDropdown) {
this.updateCurrentDropdownOffset(); this.updateCurrentDropdownOffset();
} }
......
...@@ -25,24 +25,32 @@ ...@@ -25,24 +25,32 @@
} }
bindEvents() { bindEvents() {
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this); this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
this.checkForEnterWrapper = this.checkForEnter.bind(this); this.checkForEnterWrapper = this.checkForEnter.bind(this);
this.clearSearchWrapper = this.clearSearch.bind(this); this.clearSearchWrapper = this.clearSearch.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
this.tokenChange = this.tokenChange.bind(this);
this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper); this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper); this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.addEventListener('click', this.tokenChange);
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
this.clearSearchButton.addEventListener('click', this.clearSearchWrapper); this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
} }
unbindEvents() { unbindEvents() {
this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper); this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper); this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.removeEventListener('click', this.tokenChange);
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper); this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
} }
...@@ -83,8 +91,14 @@ ...@@ -83,8 +91,14 @@
this.dropdownManager.resetDropdowns(); this.dropdownManager.resetDropdowns();
} }
handleFormSubmit(e) {
e.preventDefault();
this.search();
}
loadSearchParamsFromURL() { loadSearchParamsFromURL() {
const params = gl.utils.getUrlParamsArray(); const params = gl.utils.getUrlParamsArray();
const usernameParams = this.getUsernameParams();
const inputValues = []; const inputValues = [];
params.forEach((p) => { params.forEach((p) => {
...@@ -115,6 +129,16 @@ ...@@ -115,6 +129,16 @@
} }
inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
} else if (!match && keyParam === 'assignee_id') {
const id = parseInt(value, 10);
if (usernameParams[id]) {
inputValues.push(`assignee:@${usernameParams[id]}`);
}
} else if (!match && keyParam === 'author_id') {
const id = parseInt(value, 10);
if (usernameParams[id]) {
inputValues.push(`author:@${usernameParams[id]}`);
}
} else if (!match && keyParam === 'search') { } else if (!match && keyParam === 'search') {
inputValues.push(sanitizedValue); inputValues.push(sanitizedValue);
} }
...@@ -164,6 +188,27 @@ ...@@ -164,6 +188,27 @@
Turbolinks.visit(`?scope=all&utf8=✓&${paths.join('&')}`); Turbolinks.visit(`?scope=all&utf8=✓&${paths.join('&')}`);
} }
getUsernameParams() {
const usernamesById = {};
try {
const attribute = this.filteredSearchInput.getAttribute('data-username-params');
JSON.parse(attribute).forEach((user) => {
usernamesById[user.id] = user.username;
});
} catch (e) {
// do nothing
}
return usernamesById;
}
tokenChange() {
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
const currentDropdownRef = dropdown.reference;
this.setDropdownWrapper();
currentDropdownRef.dispatchInputEvent();
}
} }
window.gl = window.gl || {}; window.gl = window.gl || {};
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
(function() { (function() {
this.GroupAvatar = (function() { this.GroupAvatar = (function() {
function GroupAvatar() { function GroupAvatar() {
$('.js-choose-group-avatar-button').bind("click", function() { $('.js-choose-group-avatar-button').on("click", function() {
var form; var form;
form = $(this).closest("form"); form = $(this).closest("form");
return form.find(".js-group-avatar-input").click(); return form.find(".js-group-avatar-input").click();
}); });
$('.js-group-avatar-input').bind("change", function() { $('.js-group-avatar-input').on("change", function() {
var filename, form; var filename, form;
form = $(this).closest("form"); form = $(this).closest("form");
filename = $(this).val().replace(/^.*[\\\/]/, ''); filename = $(this).val().replace(/^.*[\\\/]/, '');
......
...@@ -126,7 +126,9 @@ ...@@ -126,7 +126,9 @@
MergeRequestWidget.prototype.getMergeStatus = function() { MergeRequestWidget.prototype.getMergeStatus = function() {
return $.get(this.opts.merge_check_url, function(data) { return $.get(this.opts.merge_check_url, function(data) {
return $('.mr-state-widget').replaceWith(data); var $html = $(data);
$('.mr-widget-body').replaceWith($html.find('.mr-widget-body'));
$('.mr-widget-footer').replaceWith($html.find('.mr-widget-footer'));
}); });
}; };
......
...@@ -8,31 +8,42 @@ ...@@ -8,31 +8,42 @@
* temporarily. * temporarily.
* */ * */
if ($('.accept-mr-form').length) { $(document)
$('.accept-mr-form').on('ajax:send', () => { .off('ajax:send', '.accept-mr-form')
$('.accept-mr-form :input').disable(); .on('ajax:send', '.accept-mr-form', () => {
}); $('.accept-mr-form :input').disable();
});
$('.accept_merge_request').on('click', () => { $(document)
$('.js-merge-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress'); .off('click', '.accept_merge_request')
}); .on('click', '.accept_merge_request', () => {
$('.js-merge-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress');
});
$('.merge_when_build_succeeds').on('click', () => { $(document)
$('#merge_when_build_succeeds').val('1'); .off('click', '.merge_when_build_succeeds')
}); .on('click', '.merge_when_build_succeeds', () => {
$('#merge_when_build_succeeds').val('1');
});
$('.js-merge-dropdown a').on('click', (e) => { $(document)
e.preventDefault(); .off('click', '.js-merge-dropdown a')
$(this).closest('form').submit(); .on('click', '.js-merge-dropdown a', (e) => {
}); e.preventDefault();
} else if ($('.rebase-in-progress').length) { $(e.target).closest('form').submit();
});
if ($('.rebase-in-progress').length) {
merge_request_widget.rebaseInProgress(); merge_request_widget.rebaseInProgress();
} else if ($('.rebase-mr-form').length) { } else if ($('.rebase-mr-form').length) {
$('.rebase-mr-form').on('ajax:send', () => { $(document)
.off('ajax:send', '.rebase-mr-form')
.on('ajax:send', '.rebase-mr-form', () => {
$('.rebase-mr-form :input').disable(); $('.rebase-mr-form :input').disable();
}); });
$('.js-rebase-button').on('click', () => { $(document)
.off('click', '.js-rebase-button')
.on('click', '.js-rebase-button', () => {
$('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress"); $('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress");
}); });
} else { } else {
......
...@@ -10,9 +10,9 @@ ...@@ -10,9 +10,9 @@
* The container should be the table element. * The container should be the table element.
* *
* The stage icon clicked needs to have the following HTML structure: * The stage icon clicked needs to have the following HTML structure:
* <div> * <div class="dropdown">
* <button class="dropdown js-builds-dropdown-button"></button> * <button class="dropdown js-builds-dropdown-button" data-toggle="dropdown"></button>
* <div class="js-builds-dropdown-container"></div> * <div class="js-builds-dropdown-container dropdown-menu"></div>
* </div> * </div>
*/ */
(() => { (() => {
...@@ -26,13 +26,11 @@ ...@@ -26,13 +26,11 @@
} }
/** /**
* Adds and removes the event listener. * Adds the event listener when the dropdown is opened.
* All dropdown events are fired at the .dropdown-menu's parent element.
*/ */
bindEvents() { bindEvents() {
const dropdownButtonSelector = 'button.js-builds-dropdown-button'; $(this.container).on('shown.bs.dropdown', this.getBuildsList);
$(this.container).off('click', dropdownButtonSelector, this.getBuildsList)
.on('click', dropdownButtonSelector, this.getBuildsList);
} }
/** /**
...@@ -52,11 +50,14 @@ ...@@ -52,11 +50,14 @@
/** /**
* For the clicked stage, gets the list of builds. * For the clicked stage, gets the list of builds.
* *
* @param {Object} e * All dropdown events have a relatedTarget property,
* whose value is the toggling anchor element.
*
* @param {Object} e bootstrap dropdown event
* @return {Promise} * @return {Promise}
*/ */
getBuildsList(e) { getBuildsList(e) {
const button = e.currentTarget; const button = e.relatedTarget;
const endpoint = button.dataset.stageEndpoint; const endpoint = button.dataset.stageEndpoint;
return $.ajax({ return $.ajax({
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
// //
(function () { (function () {
var lastTextareaPreviewed; var lastTextareaPreviewed;
var lastTextareaHeight = null;
var markdownPreview; var markdownPreview;
var previewButtonSelector; var previewButtonSelector;
var writeButtonSelector; var writeButtonSelector;
...@@ -104,10 +105,14 @@ ...@@ -104,10 +105,14 @@
if (!$form) { if (!$form) {
return; return;
} }
lastTextareaPreviewed = $form.find('textarea.markdown-area'); lastTextareaPreviewed = $form.find('textarea.markdown-area');
lastTextareaHeight = lastTextareaPreviewed.height();
// toggle tabs // toggle tabs
$form.find(writeButtonSelector).parent().removeClass('active'); $form.find(writeButtonSelector).parent().removeClass('active');
$form.find(previewButtonSelector).parent().addClass('active'); $form.find(previewButtonSelector).parent().addClass('active');
// toggle content // toggle content
$form.find('.md-write-holder').hide(); $form.find('.md-write-holder').hide();
$form.find('.md-preview-holder').show(); $form.find('.md-preview-holder').show();
...@@ -119,9 +124,15 @@ ...@@ -119,9 +124,15 @@
return; return;
} }
lastTextareaPreviewed = null; lastTextareaPreviewed = null;
if (lastTextareaHeight) {
$form.find('textarea.markdown-area').height(lastTextareaHeight);
}
// toggle tabs // toggle tabs
$form.find(writeButtonSelector).parent().addClass('active'); $form.find(writeButtonSelector).parent().addClass('active');
$form.find(previewButtonSelector).parent().removeClass('active'); $form.find(previewButtonSelector).parent().removeClass('active');
// toggle content // toggle content
$form.find('.md-write-holder').show(); $form.find('.md-write-holder').show();
$form.find('textarea.markdown-area').focus(); $form.find('textarea.markdown-area').focus();
......
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
return function(response) { return function(response) {
var error; var error;
if (response.errorCode) { if (response.errorCode) {
error = new U2FError(response.errorCode); error = new U2FError(response.errorCode, 'authenticate');
return _this.renderError(error); return _this.renderError(error);
} else { } else {
return _this.renderAuthenticated(JSON.stringify(response)); return _this.renderAuthenticated(JSON.stringify(response));
......
...@@ -5,21 +5,21 @@ ...@@ -5,21 +5,21 @@
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.U2FError = (function() { this.U2FError = (function() {
function U2FError(errorCode) { function U2FError(errorCode, u2fFlowType) {
this.errorCode = errorCode; this.errorCode = errorCode;
this.message = bind(this.message, this); this.message = bind(this.message, this);
this.httpsDisabled = window.location.protocol !== 'https:'; this.httpsDisabled = window.location.protocol !== 'https:';
this.u2fFlowType = u2fFlowType;
} }
U2FError.prototype.message = function() { U2FError.prototype.message = function() {
switch (false) { if (this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) {
case !(this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled): return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.';
return "U2F only works with HTTPS-enabled websites. Contact your administrator for more details."; } else if (this.errorCode === u2f.ErrorCodes.DEVICE_INELIGIBLE) {
case this.errorCode !== u2f.ErrorCodes.DEVICE_INELIGIBLE: if (this.u2fFlowType === 'authenticate') return 'This device has not been registered with us.';
return "This device has already been registered with us."; if (this.u2fFlowType === 'register') return 'This device has already been registered with us.';
default:
return "There was a problem communicating with your device.";
} }
return "There was a problem communicating with your device.";
}; };
return U2FError; return U2FError;
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
return function(response) { return function(response) {
var error; var error;
if (response.errorCode) { if (response.errorCode) {
error = new U2FError(response.errorCode); error = new U2FError(response.errorCode, 'register');
return _this.renderError(error); return _this.renderError(error);
} else { } else {
return _this.renderRegistered(JSON.stringify(response)); return _this.renderRegistered(JSON.stringify(response));
......
/* global Vue, Flash, gl */ /* global Vue, Flash, gl */
/* eslint-disable no-param-reassign, no-bitwise */ /* eslint-disable no-param-reassign */
((gl) => { ((gl) => {
gl.VueStage = Vue.extend({ gl.VueStage = Vue.extend({
...@@ -9,7 +9,20 @@ ...@@ -9,7 +9,20 @@
spinner: '<span class="fa fa-spinner fa-spin"></span>', spinner: '<span class="fa fa-spinner fa-spin"></span>',
}; };
}, },
props: ['stage', 'svgs', 'match'], props: {
stage: {
type: Object,
required: true,
},
svgs: {
type: DOMStringMap,
required: true,
},
match: {
type: Function,
required: true,
},
},
methods: { methods: {
fetchBuilds(e) { fetchBuilds(e) {
const areaExpanded = e.currentTarget.attributes['aria-expanded']; const areaExpanded = e.currentTarget.attributes['aria-expanded'];
...@@ -24,6 +37,18 @@ ...@@ -24,6 +37,18 @@
return flash; return flash;
}); });
}, },
keepGraph(e) {
const { target } = e;
if (target.className.indexOf('js-ci-action-icon') >= 0) return null;
if (
target.parentElement &&
(target.parentElement.className.indexOf('js-ci-action-icon') >= 0)
) return null;
return e.stopPropagation();
},
}, },
computed: { computed: {
buildsOrSpinner() { buildsOrSpinner() {
...@@ -64,7 +89,7 @@ ...@@ -64,7 +89,7 @@
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div class="arrow-up"></div> <div class="arrow-up"></div>
<div <div
@click='' @click='keepGraph($event)'
:class="dropdownClass" :class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu" class="js-builds-dropdown-list scrollable-menu"
v-html="buildsOrSpinner" v-html="buildsOrSpinner"
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
} }
.ci-status-icon-pending, .ci-status-icon-pending,
.ci-status-icon-failed_with_warnings,
.ci-status-icon-success_with_warnings { .ci-status-icon-success_with_warnings {
color: $gl-warning; color: $gl-warning;
......
...@@ -46,10 +46,6 @@ ...@@ -46,10 +46,6 @@
font-weight: bold; font-weight: bold;
} }
.fa-clipboard {
color: $dropdown-title-btn-color;
}
.commit-info { .commit-info {
&.branches { &.branches {
margin-left: 8px; margin-left: 8px;
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
max-width: 100%; max-width: 100%;
} }
*:first-child { *:first-child:not(.katex-display) {
margin-top: 0; margin-top: 0;
} }
......
...@@ -377,6 +377,10 @@ ...@@ -377,6 +377,10 @@
display: inline-block; display: inline-block;
padding: 5px; padding: 5px;
&:nth-of-type(7n) {
padding-right: 0;
}
.author_link { .author_link {
display: block; display: block;
} }
......
...@@ -195,10 +195,10 @@ ul.notes { ...@@ -195,10 +195,10 @@ ul.notes {
} }
.note-body { .note-body {
overflow: auto; overflow-x: auto;
overflow-y: hidden;
.note-text { .note-text {
overflow: auto;
word-wrap: break-word; word-wrap: break-word;
@include md-typography; @include md-typography;
// Reset ul style types since we're nested inside a ul already // Reset ul style types since we're nested inside a ul already
...@@ -515,7 +515,6 @@ ul.notes { ...@@ -515,7 +515,6 @@ ul.notes {
.line-resolve-all-container { .line-resolve-all-container {
.btn-group { .btn-group {
margin-top: -1px;
margin-left: -4px; margin-left: -4px;
} }
......
...@@ -198,7 +198,7 @@ ...@@ -198,7 +198,7 @@
margin: 15px 5px 0 0; margin: 15px 5px 0 0;
input { input {
height: 28px; height: 27px;
} }
} }
......
...@@ -18,7 +18,6 @@ ...@@ -18,7 +18,6 @@
.file-finder-input:hover, .file-finder-input:hover,
.issuable-search-form:hover, .issuable-search-form:hover,
.search-text-input:hover, .search-text-input:hover,
textarea:hover,
.form-control:hover { .form-control:hover {
border-color: lighten($dropdown-input-focus-border, 20%); border-color: lighten($dropdown-input-focus-border, 20%);
box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%); box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
......
...@@ -19,7 +19,8 @@ ...@@ -19,7 +19,8 @@
overflow: visible; overflow: visible;
} }
&.ci-failed { &.ci-failed,
&.ci-failed_with_warnings {
color: $gl-danger; color: $gl-danger;
border-color: $gl-danger; border-color: $gl-danger;
......
...@@ -14,12 +14,8 @@ class ConfirmationsController < Devise::ConfirmationsController ...@@ -14,12 +14,8 @@ class ConfirmationsController < Devise::ConfirmationsController
if signed_in?(resource_name) if signed_in?(resource_name)
after_sign_in_path_for(resource) after_sign_in_path_for(resource)
else else
sign_in(resource) flash[:notice] += " Please sign in."
if signed_in?(resource_name) new_session_path(resource_name)
after_sign_in_path_for(resource)
else
new_session_path(resource_name)
end
end end
end end
end end
...@@ -6,21 +6,15 @@ class Projects::HooksController < Projects::ApplicationController ...@@ -6,21 +6,15 @@ class Projects::HooksController < Projects::ApplicationController
layout "project_settings" layout "project_settings"
def index
@hooks = @project.hooks
@hook = ProjectHook.new
end
def create def create
@hook = @project.hooks.new(hook_params) @hook = @project.hooks.new(hook_params)
@hook.save @hook.save
if @hook.valid? unless @hook.valid?
redirect_to namespace_project_hooks_path(@project.namespace, @project)
else
@hooks = @project.hooks.select(&:persisted?) @hooks = @project.hooks.select(&:persisted?)
render :index flash[:alert] = @hook.errors.full_messages.join.html_safe
end end
redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
end end
def test def test
...@@ -44,7 +38,7 @@ class Projects::HooksController < Projects::ApplicationController ...@@ -44,7 +38,7 @@ class Projects::HooksController < Projects::ApplicationController
def destroy def destroy
hook.destroy hook.destroy
redirect_to namespace_project_hooks_path(@project.namespace, @project) redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
end end
private private
......
...@@ -33,6 +33,18 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -33,6 +33,18 @@ class Projects::IssuesController < Projects::ApplicationController
@labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
end end
@users = []
if params[:assignee_id].present?
assignee = User.find_by_id(params[:assignee_id])
@users.push(assignee) if assignee
end
if params[:author_id].present?
author = User.find_by_id(params[:author_id])
@users.push(author) if author
end
respond_to do |format| respond_to do |format|
format.html format.html
format.atom { render layout: false } format.atom { render layout: false }
......
...@@ -9,10 +9,6 @@ class Projects::ServicesController < Projects::ApplicationController ...@@ -9,10 +9,6 @@ class Projects::ServicesController < Projects::ApplicationController
layout "project_settings" layout "project_settings"
def index
@services = @project.find_or_initialize_services
end
def edit def edit
end end
......
module Projects
module Settings
class IntegrationsController < Projects::ApplicationController
include ServiceParams
before_action :authorize_admin_project!
layout "project_settings"
def show
@hooks = @project.hooks
@hook = ProjectHook.new
# Services
@services = @project.find_or_initialize_services
end
end
end
end
...@@ -45,6 +45,8 @@ class SearchController < ApplicationController ...@@ -45,6 +45,8 @@ class SearchController < ApplicationController
end end
@search_objects = @search_results.objects(@scope, params[:page]) @search_objects = @search_results.objects(@scope, params[:page])
check_single_commit_result
end end
def autocomplete def autocomplete
...@@ -59,4 +61,16 @@ class SearchController < ApplicationController ...@@ -59,4 +61,16 @@ class SearchController < ApplicationController
render json: search_autocomplete_opts(term).to_json render json: search_autocomplete_opts(term).to_json
end end
private
def check_single_commit_result
if @search_results.single_commit_result?
only_commit = @search_results.objects('commits').first
query = params[:search].strip.downcase
found_by_commit_sha = Commit.valid_hash?(query) && only_commit.sha.start_with?(query)
redirect_to namespace_project_commit_path(@project.namespace, @project, only_commit) if found_by_commit_sha
end
end
end end
...@@ -208,6 +208,10 @@ module GitlabRoutingHelper ...@@ -208,6 +208,10 @@ module GitlabRoutingHelper
end end
# Settings # Settings
def project_settings_integrations_path(project, *args)
namespace_project_settings_integrations_path(project.namespace, project, *args)
end
def project_settings_members_path(project, *args) def project_settings_members_path(project, *args)
namespace_project_settings_members_path(project.namespace, project, *args) namespace_project_settings_members_path(project.namespace, project, *args)
end end
......
module ServicesHelper module ServicesHelper
def service_event_description(event) def service_event_description(event)
case event case event
when "push" when "push", "push_events"
"Event will be triggered by a push to the repository" "Event will be triggered by a push to the repository"
when "tag_push" when "tag_push", "tag_push_events"
"Event will be triggered when a new tag is pushed to the repository" "Event will be triggered when a new tag is pushed to the repository"
when "note" when "note", "note_events"
"Event will be triggered when someone adds a comment" "Event will be triggered when someone adds a comment"
when "issue" when "issue", "issue_events"
"Event will be triggered when an issue is created/updated/closed" "Event will be triggered when an issue is created/updated/closed"
when "confidential_issue" when "confidential_issue", "confidential_issue_events"
"Event will be triggered when a confidential issue is created/updated/closed" "Event will be triggered when a confidential issue is created/updated/closed"
when "merge_request" when "merge_request", "merge_request_events"
"Event will be triggered when a merge request is created/updated/merged" "Event will be triggered when a merge request is created/updated/merged"
when "build" when "build", "build_events"
"Event will be triggered when a build status changes" "Event will be triggered when a build status changes"
when "wiki_page" when "wiki_page", "wiki_page_events"
"Event will be triggered when a wiki page is created/updated" "Event will be triggered when a wiki page is created/updated"
when "commit" when "commit", "commit_events"
"Event will be triggered when a commit is created/updated" "Event will be triggered when a commit is created/updated"
end end
end end
...@@ -26,4 +26,6 @@ module ServicesHelper ...@@ -26,4 +26,6 @@ module ServicesHelper
event = event.pluralize if %w[merge_request issue confidential_issue].include?(event) event = event.pluralize if %w[merge_request issue confidential_issue].include?(event)
"#{event}_events" "#{event}_events"
end end
extend self
end end
...@@ -107,15 +107,11 @@ class Notify < BaseMailer ...@@ -107,15 +107,11 @@ class Notify < BaseMailer
def mail_thread(model, headers = {}) def mail_thread(model, headers = {})
add_project_headers add_project_headers
add_unsubscription_headers_and_links
headers["X-GitLab-#{model.class.name}-ID"] = model.id headers["X-GitLab-#{model.class.name}-ID"] = model.id
headers['X-GitLab-Reply-Key'] = reply_key headers['X-GitLab-Reply-Key'] = reply_key
if !@labels_url && @sent_notification && @sent_notification.unsubscribable?
headers['List-Unsubscribe'] = "<#{unsubscribe_sent_notification_url(@sent_notification, force: true)}>"
@sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
end
if Gitlab::IncomingEmail.enabled? if Gitlab::IncomingEmail.enabled?
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key)) address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace address.display_name = @project.name_with_namespace
...@@ -171,4 +167,16 @@ class Notify < BaseMailer ...@@ -171,4 +167,16 @@ class Notify < BaseMailer
headers['X-GitLab-Project-Id'] = @project.id headers['X-GitLab-Project-Id'] = @project.id
headers['X-GitLab-Project-Path'] = @project.path_with_namespace headers['X-GitLab-Project-Path'] = @project.path_with_namespace
end end
def add_unsubscription_headers_and_links
return unless !@labels_url && @sent_notification && @sent_notification.unsubscribable?
list_unsubscribe_methods = [unsubscribe_sent_notification_url(@sent_notification, force: true)]
if Gitlab::IncomingEmail.enabled? && Gitlab::IncomingEmail.supports_wildcard?
list_unsubscribe_methods << "mailto:#{Gitlab::IncomingEmail.unsubscribe_address(reply_key)}"
end
headers['List-Unsubscribe'] = list_unsubscribe_methods.map { |e| "<#{e}>" }.join(',')
@sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
end
end end
...@@ -92,6 +92,12 @@ module Ci ...@@ -92,6 +92,12 @@ module Ci
end end
state_machine :status do state_machine :status do
after_transition any => [:pending] do |build|
build.run_after_commit do
BuildQueueWorker.perform_async(id)
end
end
after_transition pending: :running do |build| after_transition pending: :running do |build|
build.run_after_commit do build.run_after_commit do
BuildHooksWorker.perform_async(id) BuildHooksWorker.perform_async(id)
......
...@@ -128,16 +128,21 @@ module Ci ...@@ -128,16 +128,21 @@ module Ci
end end
def stages def stages
# TODO, this needs refactoring, see gitlab-ce#26481.
stages_query = statuses
.group('stage').select(:stage).order('max(stage_idx)')
status_sql = statuses.latest.where('stage=sg.stage').status_sql status_sql = statuses.latest.where('stage=sg.stage').status_sql
stages_query = statuses.group('stage').select(:stage) warnings_sql = statuses.latest.select('COUNT(*) > 0')
.order('max(stage_idx)') .where('stage=sg.stage').failed_but_allowed.to_sql
stages_with_statuses = CommitStatus.from(stages_query, :sg). stages_with_statuses = CommitStatus.from(stages_query, :sg)
pluck('sg.stage', status_sql) .pluck('sg.stage', status_sql, "(#{warnings_sql})")
stages_with_statuses.map do |stage| stages_with_statuses.map do |stage|
Ci::Stage.new(self, name: stage.first, status: stage.last) Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)])
end end
end end
......
...@@ -2,6 +2,7 @@ module Ci ...@@ -2,6 +2,7 @@ module Ci
class Runner < ActiveRecord::Base class Runner < ActiveRecord::Base
extend Ci::Model extend Ci::Model
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
LAST_CONTACT_TIME = 1.hour.ago LAST_CONTACT_TIME = 1.hour.ago
AVAILABLE_SCOPES = %w[specific shared active paused online] AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active run_untagged locked] FORM_EDITABLE = %i[description tag_list active run_untagged locked]
...@@ -21,6 +22,8 @@ module Ci ...@@ -21,6 +22,8 @@ module Ci
scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) } scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) }
scope :ordered, ->() { order(id: :desc) } scope :ordered, ->() { order(id: :desc) }
after_save :tick_runner_queue, if: :form_editable_changed?
scope :owned_or_shared, ->(project_id) do scope :owned_or_shared, ->(project_id) do
joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id') joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
.where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
...@@ -122,8 +125,38 @@ module Ci ...@@ -122,8 +125,38 @@ module Ci
] ]
end end
def tick_runner_queue
SecureRandom.hex.tap do |new_update|
Gitlab::Redis.with do |redis|
redis.set(runner_queue_key, new_update, ex: RUNNER_QUEUE_EXPIRY_TIME)
end
end
end
def ensure_runner_queue_value
Gitlab::Redis.with do |redis|
value = SecureRandom.hex
redis.set(runner_queue_key, value, ex: RUNNER_QUEUE_EXPIRY_TIME, nx: true)
redis.get(runner_queue_key)
end
end
def is_runner_queue_value_latest?(value)
ensure_runner_queue_value == value if value.present?
end
private private
def runner_queue_key
"runner:build_queue:#{self.token}"
end
def form_editable_changed?
FORM_EDITABLE.any? do |editable|
public_send("#{editable}_changed?")
end
end
def tag_constraints def tag_constraints
unless has_tags? || run_untagged? unless has_tags? || run_untagged?
errors.add(:tags_list, errors.add(:tags_list,
......
...@@ -8,10 +8,11 @@ module Ci ...@@ -8,10 +8,11 @@ module Ci
delegate :project, to: :pipeline delegate :project, to: :pipeline
def initialize(pipeline, name:, status: nil) def initialize(pipeline, name:, status: nil, warnings: nil)
@pipeline = pipeline @pipeline = pipeline
@name = name @name = name
@status = status @status = status
@warnings = warnings
end end
def to_param def to_param
...@@ -39,5 +40,17 @@ module Ci ...@@ -39,5 +40,17 @@ module Ci
def builds def builds
@builds ||= pipeline.builds.where(stage: name) @builds ||= pipeline.builds.where(stage: name)
end end
def success?
status.to_s == 'success'
end
def has_warnings?
if @warnings.nil?
statuses.latest.failed_but_allowed.any?
else
@warnings
end
end
end end
end end
...@@ -21,6 +21,9 @@ class Commit ...@@ -21,6 +21,9 @@ class Commit
DIFF_HARD_LIMIT_FILES = 1000 DIFF_HARD_LIMIT_FILES = 1000
DIFF_HARD_LIMIT_LINES = 50000 DIFF_HARD_LIMIT_LINES = 50000
# The SHA can be between 7 and 40 hex characters.
COMMIT_SHA_PATTERN = '\h{7,40}'
class << self class << self
def decorate(commits, project) def decorate(commits, project)
commits.map do |commit| commits.map do |commit|
...@@ -52,6 +55,10 @@ class Commit ...@@ -52,6 +55,10 @@ class Commit
def from_hash(hash, project) def from_hash(hash, project)
new(Gitlab::Git::Commit.new(hash), project) new(Gitlab::Git::Commit.new(hash), project)
end end
def valid_hash?(key)
!!(/\A#{COMMIT_SHA_PATTERN}\z/ =~ key)
end
end end
attr_accessor :raw attr_accessor :raw
...@@ -77,8 +84,6 @@ class Commit ...@@ -77,8 +84,6 @@ class Commit
# Pattern used to extract commit references from text # Pattern used to extract commit references from text
# #
# The SHA can be between 7 and 40 hex characters.
#
# This pattern supports cross-project references. # This pattern supports cross-project references.
def self.reference_pattern def self.reference_pattern
@reference_pattern ||= %r{ @reference_pattern ||= %r{
...@@ -88,7 +93,7 @@ class Commit ...@@ -88,7 +93,7 @@ class Commit
end end
def self.link_reference_pattern def self.link_reference_pattern
@link_reference_pattern ||= super("commit", /(?<commit>\h{7,40})/) @link_reference_pattern ||= super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})/)
end end
def to_reference(from_project = nil, full: false) def to_reference(from_project = nil, full: false)
......
module HasStatus module HasStatus
extend ActiveSupport::Concern extend ActiveSupport::Concern
DEFAULT_STATUS = 'created'
AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped] AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped]
STARTED_STATUSES = %w[running success failed skipped] STARTED_STATUSES = %w[running success failed skipped]
ACTIVE_STATUSES = %w[pending running] ACTIVE_STATUSES = %w[pending running]
......
...@@ -11,7 +11,7 @@ module Taskable ...@@ -11,7 +11,7 @@ module Taskable
INCOMPLETE = 'incomplete'.freeze INCOMPLETE = 'incomplete'.freeze
ITEM_PATTERN = / ITEM_PATTERN = /
^ ^
(?:\s*[-+*]|(?:\d+\.))? # optional list prefix \s*(?:[-+*]|(?:\d+\.))? # optional list prefix
\s* # optional whitespace prefix \s* # optional whitespace prefix
(\[\s\]|\[[xX]\]) # checkbox (\[\s\]|\[[xX]\]) # checkbox
(\s.+) # followed by whitespace and some text. (\s.+) # followed by whitespace and some text.
......
...@@ -4,6 +4,8 @@ class Key < ActiveRecord::Base ...@@ -4,6 +4,8 @@ class Key < ActiveRecord::Base
include AfterCommitQueue include AfterCommitQueue
include Sortable include Sortable
LAST_USED_AT_REFRESH_TIME = 1.day.to_i
belongs_to :user belongs_to :user
before_validation :generate_fingerprint before_validation :generate_fingerprint
...@@ -50,7 +52,10 @@ class Key < ActiveRecord::Base ...@@ -50,7 +52,10 @@ class Key < ActiveRecord::Base
end end
def update_last_used_at def update_last_used_at
UseKeyWorker.perform_async(self.id) lease = Gitlab::ExclusiveLease.new("key_update_last_used_at:#{id}", timeout: LAST_USED_AT_REFRESH_TIME)
return unless lease.try_obtain
UseKeyWorker.perform_async(id)
end end
def add_to_shell def add_to_shell
......
...@@ -130,6 +130,8 @@ class Namespace < ActiveRecord::Base ...@@ -130,6 +130,8 @@ class Namespace < ActiveRecord::Base
Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
remove_exports!
# If repositories moved successfully we need to # If repositories moved successfully we need to
# send update instructions to users. # send update instructions to users.
# However we cannot allow rollback since we moved namespace dir # However we cannot allow rollback since we moved namespace dir
...@@ -214,6 +216,8 @@ class Namespace < ActiveRecord::Base ...@@ -214,6 +216,8 @@ class Namespace < ActiveRecord::Base
GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path) GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path)
end end
end end
remove_exports!
end end
def refresh_access_of_projects_invited_groups def refresh_access_of_projects_invited_groups
...@@ -226,4 +230,20 @@ class Namespace < ActiveRecord::Base ...@@ -226,4 +230,20 @@ class Namespace < ActiveRecord::Base
def full_path_changed? def full_path_changed?
path_changed? || parent_id_changed? path_changed? || parent_id_changed?
end end
def remove_exports!
Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
end
def export_path
File.join(Gitlab::ImportExport.storage_path, full_path_was)
end
def full_path_was
if parent
parent.full_path + '/' + path_was
else
path_was
end
end
end end
...@@ -121,8 +121,6 @@ class Project < ActiveRecord::Base ...@@ -121,8 +121,6 @@ class Project < ActiveRecord::Base
# Merge Requests for target project should be removed with it # Merge Requests for target project should be removed with it
has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id' has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id'
# Merge requests from source project should be kept when source project was removed
has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
has_many :issues, dependent: :destroy has_many :issues, dependent: :destroy
has_many :labels, dependent: :destroy, class_name: 'ProjectLabel' has_many :labels, dependent: :destroy, class_name: 'ProjectLabel'
has_many :services, dependent: :destroy has_many :services, dependent: :destroy
......
...@@ -25,7 +25,7 @@ You can create a Personal Access Token here: ...@@ -25,7 +25,7 @@ You can create a Personal Access Token here:
http://app.asana.com/-/account_api' http://app.asana.com/-/account_api'
end end
def to_param def self.to_param
'asana' 'asana'
end end
...@@ -44,7 +44,7 @@ http://app.asana.com/-/account_api' ...@@ -44,7 +44,7 @@ http://app.asana.com/-/account_api'
] ]
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -12,7 +12,7 @@ class AssemblaService < Service ...@@ -12,7 +12,7 @@ class AssemblaService < Service
'Project Management Software (Source Commits Endpoint)' 'Project Management Software (Source Commits Endpoint)'
end end
def to_param def self.to_param
'assembla' 'assembla'
end end
...@@ -23,7 +23,7 @@ class AssemblaService < Service ...@@ -23,7 +23,7 @@ class AssemblaService < Service
] ]
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -40,7 +40,7 @@ class BambooService < CiService ...@@ -40,7 +40,7 @@ class BambooService < CiService
'You must set up automatic revision labeling and a repository trigger in Bamboo.' 'You must set up automatic revision labeling and a repository trigger in Bamboo.'
end end
def to_param def self.to_param
'bamboo' 'bamboo'
end end
...@@ -56,10 +56,6 @@ class BambooService < CiService ...@@ -56,10 +56,6 @@ class BambooService < CiService
] ]
end end
def supported_events
%w(push)
end
def build_page(sha, ref) def build_page(sha, ref)
with_reactive_cache(sha, ref) {|cached| cached[:build_page] } with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
end end
......
...@@ -19,7 +19,7 @@ class BugzillaService < IssueTrackerService ...@@ -19,7 +19,7 @@ class BugzillaService < IssueTrackerService
end end
end end
def to_param def self.to_param
'bugzilla' 'bugzilla'
end end
end end
...@@ -24,10 +24,6 @@ class BuildkiteService < CiService ...@@ -24,10 +24,6 @@ class BuildkiteService < CiService
hook.save hook.save
end end
def supported_events
%w(push)
end
def execute(data) def execute(data)
return unless supported_events.include?(data[:object_kind]) return unless supported_events.include?(data[:object_kind])
...@@ -54,7 +50,7 @@ class BuildkiteService < CiService ...@@ -54,7 +50,7 @@ class BuildkiteService < CiService
'Continuous integration and deployments' 'Continuous integration and deployments'
end end
def to_param def self.to_param
'buildkite' 'buildkite'
end end
......
...@@ -19,11 +19,11 @@ class BuildsEmailService < Service ...@@ -19,11 +19,11 @@ class BuildsEmailService < Service
'Email the builds status to a list of recipients.' 'Email the builds status to a list of recipients.'
end end
def to_param def self.to_param
'builds_email' 'builds_email'
end end
def supported_events def self.supported_events
%w(build) %w(build)
end end
......
...@@ -12,7 +12,7 @@ class CampfireService < Service ...@@ -12,7 +12,7 @@ class CampfireService < Service
'Simple web-based real-time group chat' 'Simple web-based real-time group chat'
end end
def to_param def self.to_param
'campfire' 'campfire'
end end
...@@ -24,7 +24,7 @@ class CampfireService < Service ...@@ -24,7 +24,7 @@ class CampfireService < Service
] ]
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -25,7 +25,7 @@ class ChatNotificationService < Service ...@@ -25,7 +25,7 @@ class ChatNotificationService < Service
valid? valid?
end end
def supported_events def self.supported_events
%w[push issue confidential_issue merge_request note tag_push %w[push issue confidential_issue merge_request note tag_push
build pipeline wiki_page] build pipeline wiki_page]
end end
...@@ -82,19 +82,19 @@ class ChatNotificationService < Service ...@@ -82,19 +82,19 @@ class ChatNotificationService < Service
def get_message(object_kind, data) def get_message(object_kind, data)
case object_kind case object_kind
when "push", "tag_push" when "push", "tag_push"
PushMessage.new(data) ChatMessage::PushMessage.new(data)
when "issue" when "issue"
IssueMessage.new(data) unless is_update?(data) ChatMessage::IssueMessage.new(data) unless is_update?(data)
when "merge_request" when "merge_request"
MergeMessage.new(data) unless is_update?(data) ChatMessage::MergeMessage.new(data) unless is_update?(data)
when "note" when "note"
NoteMessage.new(data) ChatMessage::NoteMessage.new(data)
when "build" when "build"
BuildMessage.new(data) if should_build_be_notified?(data) ChatMessage::BuildMessage.new(data) if should_build_be_notified?(data)
when "pipeline" when "pipeline"
PipelineMessage.new(data) if should_pipeline_be_notified?(data) ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page" when "wiki_page"
WikiPageMessage.new(data) ChatMessage::WikiPageMessage.new(data)
end end
end end
......
...@@ -13,8 +13,8 @@ class ChatSlashCommandsService < Service ...@@ -13,8 +13,8 @@ class ChatSlashCommandsService < Service
ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end end
def supported_events def self.supported_events
[] %w()
end end
def can_test? def can_test?
......
...@@ -8,7 +8,7 @@ class CiService < Service ...@@ -8,7 +8,7 @@ class CiService < Service
self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -23,7 +23,7 @@ class CustomIssueTrackerService < IssueTrackerService ...@@ -23,7 +23,7 @@ class CustomIssueTrackerService < IssueTrackerService
end end
end end
def to_param def self.to_param
'custom_issue_tracker' 'custom_issue_tracker'
end end
......
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
class DeploymentService < Service class DeploymentService < Service
default_value_for :category, 'deployment' default_value_for :category, 'deployment'
def supported_events def self.supported_events
[] %w()
end end
def predefined_variables def predefined_variables
......
...@@ -32,7 +32,7 @@ class DroneCiService < CiService ...@@ -32,7 +32,7 @@ class DroneCiService < CiService
true true
end end
def supported_events def self.supported_events
%w(push merge_request tag_push) %w(push merge_request tag_push)
end end
...@@ -87,7 +87,7 @@ class DroneCiService < CiService ...@@ -87,7 +87,7 @@ class DroneCiService < CiService
'Drone is a Continuous Integration platform built on Docker, written in Go' 'Drone is a Continuous Integration platform built on Docker, written in Go'
end end
def to_param def self.to_param
'drone_ci' 'drone_ci'
end end
......
...@@ -12,11 +12,11 @@ class EmailsOnPushService < Service ...@@ -12,11 +12,11 @@ class EmailsOnPushService < Service
'Email the commits and diff of each push to a list of recipients.' 'Email the commits and diff of each push to a list of recipients.'
end end
def to_param def self.to_param
'emails_on_push' 'emails_on_push'
end end
def supported_events def self.supported_events
%w(push tag_push) %w(push tag_push)
end end
......
...@@ -13,7 +13,7 @@ class ExternalWikiService < Service ...@@ -13,7 +13,7 @@ class ExternalWikiService < Service
'Replaces the link to the internal wiki with a link to an external wiki.' 'Replaces the link to the internal wiki with a link to an external wiki.'
end end
def to_param def self.to_param
'external_wiki' 'external_wiki'
end end
...@@ -29,4 +29,8 @@ class ExternalWikiService < Service ...@@ -29,4 +29,8 @@ class ExternalWikiService < Service
nil nil
end end
end end
def self.supported_events
%w()
end
end end
...@@ -12,7 +12,7 @@ class FlowdockService < Service ...@@ -12,7 +12,7 @@ class FlowdockService < Service
'Flowdock is a collaboration web app for technical teams.' 'Flowdock is a collaboration web app for technical teams.'
end end
def to_param def self.to_param
'flowdock' 'flowdock'
end end
...@@ -22,7 +22,7 @@ class FlowdockService < Service ...@@ -22,7 +22,7 @@ class FlowdockService < Service
] ]
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -12,7 +12,7 @@ class GemnasiumService < Service ...@@ -12,7 +12,7 @@ class GemnasiumService < Service
'Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.' 'Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.'
end end
def to_param def self.to_param
'gemnasium' 'gemnasium'
end end
...@@ -23,7 +23,7 @@ class GemnasiumService < Service ...@@ -23,7 +23,7 @@ class GemnasiumService < Service
] ]
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -7,7 +7,7 @@ class GitlabIssueTrackerService < IssueTrackerService ...@@ -7,7 +7,7 @@ class GitlabIssueTrackerService < IssueTrackerService
default_value_for :default, true default_value_for :default, true
def to_param def self.to_param
'gitlab' 'gitlab'
end end
......
...@@ -27,7 +27,7 @@ class HipchatService < Service ...@@ -27,7 +27,7 @@ class HipchatService < Service
'Private group chat and IM' 'Private group chat and IM'
end end
def to_param def self.to_param
'hipchat' 'hipchat'
end end
...@@ -45,7 +45,7 @@ class HipchatService < Service ...@@ -45,7 +45,7 @@ class HipchatService < Service
] ]
end end
def supported_events def self.supported_events
%w(push issue confidential_issue merge_request note tag_push build) %w(push issue confidential_issue merge_request note tag_push build)
end end
......
...@@ -17,11 +17,11 @@ class IrkerService < Service ...@@ -17,11 +17,11 @@ class IrkerService < Service
'gateway.' 'gateway.'
end end
def to_param def self.to_param
'irker' 'irker'
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -57,7 +57,7 @@ class IssueTrackerService < Service ...@@ -57,7 +57,7 @@ class IssueTrackerService < Service
end end
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -12,7 +12,7 @@ class JiraService < IssueTrackerService ...@@ -12,7 +12,7 @@ class JiraService < IssueTrackerService
# This is confusing, but JiraService does not really support these events. # This is confusing, but JiraService does not really support these events.
# The values here are required to display correct options in the service # The values here are required to display correct options in the service
# configuration screen. # configuration screen.
def supported_events def self.supported_events
%w(commit merge_request) %w(commit merge_request)
end end
...@@ -81,7 +81,7 @@ class JiraService < IssueTrackerService ...@@ -81,7 +81,7 @@ class JiraService < IssueTrackerService
end end
end end
def to_param def self.to_param
'jira' 'jira'
end end
......
...@@ -52,7 +52,7 @@ class KubernetesService < DeploymentService ...@@ -52,7 +52,7 @@ class KubernetesService < DeploymentService
'deployments with `app=$CI_ENVIRONMENT_SLUG`' 'deployments with `app=$CI_ENVIRONMENT_SLUG`'
end end
def to_param def self.to_param
'kubernetes' 'kubernetes'
end end
......
...@@ -7,7 +7,7 @@ class MattermostService < ChatNotificationService ...@@ -7,7 +7,7 @@ class MattermostService < ChatNotificationService
'Receive event notifications in Mattermost' 'Receive event notifications in Mattermost'
end end
def to_param def self.to_param
'mattermost' 'mattermost'
end end
...@@ -36,6 +36,6 @@ class MattermostService < ChatNotificationService ...@@ -36,6 +36,6 @@ class MattermostService < ChatNotificationService
end end
def default_channel_placeholder def default_channel_placeholder
"#town-square" "town-square"
end end
end end
...@@ -15,7 +15,7 @@ class MattermostSlashCommandsService < ChatSlashCommandsService ...@@ -15,7 +15,7 @@ class MattermostSlashCommandsService < ChatSlashCommandsService
"Perform common operations on GitLab in Mattermost" "Perform common operations on GitLab in Mattermost"
end end
def to_param def self.to_param
'mattermost_slash_commands' 'mattermost_slash_commands'
end end
......
...@@ -15,11 +15,11 @@ class PipelinesEmailService < Service ...@@ -15,11 +15,11 @@ class PipelinesEmailService < Service
'Email the pipelines status to a list of recipients.' 'Email the pipelines status to a list of recipients.'
end end
def to_param def self.to_param
'pipelines_email' 'pipelines_email'
end end
def supported_events def self.supported_events
%w[pipeline] %w[pipeline]
end end
......
...@@ -14,7 +14,7 @@ class PivotaltrackerService < Service ...@@ -14,7 +14,7 @@ class PivotaltrackerService < Service
'Project Management Software (Source Commits Endpoint)' 'Project Management Software (Source Commits Endpoint)'
end end
def to_param def self.to_param
'pivotaltracker' 'pivotaltracker'
end end
...@@ -34,7 +34,7 @@ class PivotaltrackerService < Service ...@@ -34,7 +34,7 @@ class PivotaltrackerService < Service
] ]
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -13,7 +13,7 @@ class PushoverService < Service ...@@ -13,7 +13,7 @@ class PushoverService < Service
'Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.' 'Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.'
end end
def to_param def self.to_param
'pushover' 'pushover'
end end
...@@ -61,7 +61,7 @@ class PushoverService < Service ...@@ -61,7 +61,7 @@ class PushoverService < Service
] ]
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -19,7 +19,7 @@ class RedmineService < IssueTrackerService ...@@ -19,7 +19,7 @@ class RedmineService < IssueTrackerService
end end
end end
def to_param def self.to_param
'redmine' 'redmine'
end end
end end
...@@ -7,7 +7,7 @@ class SlackService < ChatNotificationService ...@@ -7,7 +7,7 @@ class SlackService < ChatNotificationService
'Receive event notifications in Slack' 'Receive event notifications in Slack'
end end
def to_param def self.to_param
'slack' 'slack'
end end
......
...@@ -9,7 +9,7 @@ class SlackSlashCommandsService < ChatSlashCommandsService ...@@ -9,7 +9,7 @@ class SlackSlashCommandsService < ChatSlashCommandsService
"Perform common operations on GitLab in Slack" "Perform common operations on GitLab in Slack"
end end
def to_param def self.to_param
'slack_slash_commands' 'slack_slash_commands'
end end
......
...@@ -43,14 +43,10 @@ class TeamcityService < CiService ...@@ -43,14 +43,10 @@ class TeamcityService < CiService
'requests build, that setting is in the vsc root advanced settings.' 'requests build, that setting is in the vsc root advanced settings.'
end end
def to_param def self.to_param
'teamcity' 'teamcity'
end end
def supported_events
%w(push)
end
def fields def fields
[ [
{ type: 'text', name: 'teamcity_url', { type: 'text', name: 'teamcity_url',
......
...@@ -76,6 +76,11 @@ class Service < ActiveRecord::Base ...@@ -76,6 +76,11 @@ class Service < ActiveRecord::Base
def to_param def to_param
# implement inside child # implement inside child
self.class.to_param
end
def self.to_param
raise NotImplementedError
end end
def fields def fields
...@@ -92,7 +97,11 @@ class Service < ActiveRecord::Base ...@@ -92,7 +97,11 @@ class Service < ActiveRecord::Base
end end
def event_names def event_names
supported_events.map { |event| "#{event}_events" } self.class.event_names
end
def self.event_names
self.supported_events.map { |event| "#{event}_events" }
end end
def event_field(event) def event_field(event)
...@@ -104,6 +113,10 @@ class Service < ActiveRecord::Base ...@@ -104,6 +113,10 @@ class Service < ActiveRecord::Base
end end
def supported_events def supported_events
self.class.supported_events
end
def self.supported_events
%w(push tag_push issue confidential_issue merge_request wiki_page) %w(push tag_push issue confidential_issue merge_request wiki_page)
end end
......
...@@ -40,10 +40,12 @@ class PipelineEntity < Grape::Entity ...@@ -40,10 +40,12 @@ class PipelineEntity < Grape::Entity
end end
expose :path do |pipeline| expose :path do |pipeline|
namespace_project_tree_path( if pipeline.ref
pipeline.project.namespace, namespace_project_tree_path(
pipeline.project, pipeline.project.namespace,
id: pipeline.ref) pipeline.project,
id: pipeline.ref)
end
end end
expose :tag?, as: :tag expose :tag?, as: :tag
......
module Ci
class UpdateBuildQueueService
def execute(build)
build.project.runners.each do |runner|
if runner.can_pick?(build)
runner.tick_runner_queue
end
end
return unless build.project.shared_runners_enabled?
Ci::Runner.shared.each do |runner|
if runner.can_pick?(build)
runner.tick_runner_queue
end
end
end
end
end
...@@ -38,15 +38,13 @@ module MergeRequests ...@@ -38,15 +38,13 @@ module MergeRequests
private private
def merge_requests_for(branch) # Returns all origin and fork merge requests from `@project` satisfying passed arguments.
origin_merge_requests = @project.origin_merge_requests def merge_requests_for(source_branch, mr_states: [:opened])
.opened.where(source_branch: branch).to_a MergeRequest
.with_state(mr_states)
fork_merge_requests = @project.fork_merge_requests .where(source_branch: source_branch, source_project_id: @project.id)
.opened.where(source_branch: branch).to_a .preload(:source_project) # we don't need a #includes since we're just preloading for the #select
.select(&:source_project)
(origin_merge_requests + fork_merge_requests)
.uniq.select(&:source_project)
end end
def pipeline_merge_requests(pipeline) def pipeline_merge_requests(pipeline)
......
...@@ -42,7 +42,7 @@ module MergeRequests ...@@ -42,7 +42,7 @@ module MergeRequests
commit_ids.include?(merge_request.diff_head_sha) commit_ids.include?(merge_request.diff_head_sha)
end end
merge_requests.uniq.select(&:source_project).each do |merge_request| filter_merge_requests(merge_requests).each do |merge_request|
MergeRequests::PostMergeService. MergeRequests::PostMergeService.
new(merge_request.target_project, @current_user). new(merge_request.target_project, @current_user).
execute(merge_request) execute(merge_request)
...@@ -58,10 +58,13 @@ module MergeRequests ...@@ -58,10 +58,13 @@ module MergeRequests
def reload_merge_requests def reload_merge_requests
merge_requests = @project.merge_requests.opened. merge_requests = @project.merge_requests.opened.
by_source_or_target_branch(@branch_name).to_a by_source_or_target_branch(@branch_name).to_a
merge_requests += fork_merge_requests
merge_requests = filter_merge_requests(merge_requests)
merge_requests.each do |merge_request| # Fork merge requests
merge_requests += MergeRequest.opened
.where(source_branch: @branch_name, source_project: @project)
.where.not(target_project: @project).to_a
filter_merge_requests(merge_requests).each do |merge_request|
if merge_request.source_branch == @branch_name || force_push? if merge_request.source_branch == @branch_name || force_push?
merge_request.reload_diff merge_request.reload_diff
else else
...@@ -175,16 +178,7 @@ module MergeRequests ...@@ -175,16 +178,7 @@ module MergeRequests
end end
def merge_requests_for_source_branch def merge_requests_for_source_branch
@source_merge_requests ||= begin @source_merge_requests ||= merge_requests_for(@branch_name)
merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a
merge_requests += fork_merge_requests
filter_merge_requests(merge_requests)
end
end
def fork_merge_requests
@fork_merge_requests ||= @project.fork_merge_requests.opened.
where(source_branch: @branch_name).to_a
end end
def branch_added? def branch_added?
......
...@@ -8,14 +8,10 @@ ...@@ -8,14 +8,10 @@
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
%span %span
Deploy Keys Deploy Keys
= nav_link(controller: :hooks) do = nav_link(controller: :integrations) do
= link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do = link_to namespace_project_settings_integrations_path(@project.namespace, @project), title: 'Integrations' do
%span %span
Webhooks Integrations
= nav_link(controller: :services) do
= link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do
%span
Services
= nav_link(controller: :protected_branches) do = nav_link(controller: :protected_branches) do
= link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do
%span %span
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
%a.close{ href: "#", "data-dismiss" => "modal" } × %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title== #{label} this #{commit.change_type_title(current_user)} %h3.page-title== #{label} this #{commit.change_type_title(current_user)}
.modal-body .modal-body
= form_tag [type.underscore, @project.namespace, @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do
.form-group.branch .form-group.branch
= label_tag 'target_branch', target_label, class: 'control-label' = label_tag 'target_branch', target_label, class: 'control-label'
.col-sm-10 .col-sm-10
......
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('merge_request_widget/ci_bundle.js')
%h4 %h4
Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)}
to be merged automatically when the pipeline succeeds. to be merged automatically when the pipeline succeeds.
......
- page_title "Services"
.row.prepend-top-default.append-bottom-default .row.prepend-top-default.append-bottom-default
.col-lg-3 .col-lg-3
%h4.prepend-top-0 %h4.prepend-top-0
......
- page_title 'Integrations'
= render 'projects/hooks/index'
= render 'projects/services/index'
%button.choose-btn.btn.btn-sm.js-choose-group-avatar-button %button.choose-btn.btn.btn-sm.js-choose-group-avatar-button{ type: 'button' }
%i.fa.fa-paperclip %i.fa.fa-paperclip
%span Choose File ... %span Choose File ...
&nbsp; &nbsp;
......
...@@ -11,13 +11,13 @@ ...@@ -11,13 +11,13 @@
class: "check_all_issues left" class: "check_all_issues left"
.issues-other-filters.filtered-search-container .issues-other-filters.filtered-search-container
.filtered-search-input-container .filtered-search-input-container
%input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id } %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]) }
= icon('filter') = icon('filter')
%button.clear-search.hidden{ type: 'button' } %button.clear-search.hidden{ type: 'button' }
= icon('times') = icon('times')
#js-dropdown-hint.dropdown-menu.hint-dropdown #js-dropdown-hint.dropdown-menu.hint-dropdown
%ul{ 'data-dropdown' => true } %ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => '' } %li.filter-dropdown-item{ 'data-action' => 'submit' }
%button.btn.btn-link %button.btn.btn-link
= icon('search') = icon('search')
%span %span
...@@ -47,6 +47,10 @@ ...@@ -47,6 +47,10 @@
%li.filter-dropdown-item{ 'data-value' => 'none' } %li.filter-dropdown-item{ 'data-value' => 'none' }
%button.btn.btn-link %button.btn.btn-link
No Assignee No Assignee
- if current_user
%li.filter-dropdown-item{ 'data-value' => current_user.to_reference }
%button.btn.btn-link
Assigned to me
%li.divider %li.divider
%ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
%li.filter-dropdown-item %li.filter-dropdown-item
...@@ -121,7 +125,13 @@ ...@@ -121,7 +125,13 @@
new MilestoneSelect(); new MilestoneSelect();
new IssueStatusSelect(); new IssueStatusSelect();
new SubscriptionSelect(); new SubscriptionSelect();
$('form.filter-form').on('submit', function (event) {
event.preventDefault(); $(document).off('page:restore').on('page:restore', function (event) {
Turbolinks.visit(this.action + '&' + $(this).serialize()); if (gl.FilteredSearchManager) {
new gl.FilteredSearchManager();
}
Issuable.init();
new gl.IssuableBulkActions({
prefixId: 'issue_',
});
}); });
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment