Commit 5f47d815 authored by Nick Thomas's avatar Nick Thomas

Merge CE -> EE with particular attention to CacheMarkdownField

parents 71a3d047 110e15da
...@@ -3,25 +3,34 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -3,25 +3,34 @@ Please view this file on the master branch, on stable branches it's out of date.
v 8.13.0 (unreleased) v 8.13.0 (unreleased)
- Update runner version only when updating contacted_at - Update runner version only when updating contacted_at
- Add link from system note to compare with previous version - Add link from system note to compare with previous version
- Improve issue load time performance by avoiding ORDER BY in find_by call
- Use gitlab-shell v3.6.2 (GIT TRACE logging) - Use gitlab-shell v3.6.2 (GIT TRACE logging)
- Fix centering of custom header logos (Ashley Dumaine) - Fix centering of custom header logos (Ashley Dumaine)
- AbstractReferenceFilter caches project_refs on RequestStore when active - AbstractReferenceFilter caches project_refs on RequestStore when active
- Replaced the check sign to arrow in the show build view. !6501 - Replaced the check sign to arrow in the show build view. !6501
- Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
- Speed-up group milestones show page - Speed-up group milestones show page
- Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
- Add tag shortcut from the Commit page. !6543
- Keep refs for each deployment - Keep refs for each deployment
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller) - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
- Add more tests for calendar contribution (ClemMakesApps) - Add more tests for calendar contribution (ClemMakesApps)
- Cache rendered markdown in the database, rather than Redis
- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
- Simplify Mentionable concern instance methods - Simplify Mentionable concern instance methods
- Fix permission for setting an issue's due date - Fix permission for setting an issue's due date
- API: Multi-file commit !6096 (mahcsig)
- Revert "Label list shows all issues (opened or closed) with that label"
- Expose expires_at field when sharing project on API - Expose expires_at field when sharing project on API
- Fix VueJS template tags being rendered in code comments - Fix VueJS template tags being rendered in code comments
- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell) - Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
- Add Issue Board API support (andrebsguedes)
- Allow the Koding integration to be configured through the API - Allow the Koding integration to be configured through the API
- Add new issue button to each list on Issues Board
- Added soft wrap button to repository file/blob editor - Added soft wrap button to repository file/blob editor
- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps) - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
- Fix todos page mobile viewport layout (ClemMakesApps) - Fix todos page mobile viewport layout (ClemMakesApps)
- Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps)
- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison) - Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska) - Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
- Fix that manual jobs would no longer block jobs in the next stage. !6604 - Fix that manual jobs would no longer block jobs in the next stage. !6604
...@@ -30,6 +39,7 @@ v 8.13.0 (unreleased) ...@@ -30,6 +39,7 @@ v 8.13.0 (unreleased)
- Replace `alias_method_chain` with `Module#prepend` - Replace `alias_method_chain` with `Module#prepend`
- Enable GitLab Import/Export for non-admin users. - Enable GitLab Import/Export for non-admin users.
- Preserve label filters when sorting !6136 (Joseph Frazier) - Preserve label filters when sorting !6136 (Joseph Frazier)
- MergeRequest#new form load diff asynchronously
- Only update issuable labels if they have been changed - Only update issuable labels if they have been changed
- Take filters in account in issuable counters. !6496 - Take filters in account in issuable counters. !6496
- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*) - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
...@@ -37,6 +47,7 @@ v 8.13.0 (unreleased) ...@@ -37,6 +47,7 @@ v 8.13.0 (unreleased)
- Append issue template to existing description !6149 (Joseph Frazier) - Append issue template to existing description !6149 (Joseph Frazier)
- Trending projects now only show public projects and the list of projects is cached for a day - Trending projects now only show public projects and the list of projects is cached for a day
- Revoke button in Applications Settings underlines on hover. - Revoke button in Applications Settings underlines on hover.
- Use higher size on Gitlab::Redis connection pool on Sidekiq servers
- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska) - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
- Fix Long commit messages overflow viewport in file tree - Fix Long commit messages overflow viewport in file tree
- Revert avoid touching file system on Build#artifacts? - Revert avoid touching file system on Build#artifacts?
...@@ -44,8 +55,10 @@ v 8.13.0 (unreleased) ...@@ -44,8 +55,10 @@ v 8.13.0 (unreleased)
- Add broadcast messages and alerts below sub-nav - Add broadcast messages and alerts below sub-nav
- Better empty state for Groups view - Better empty state for Groups view
- Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe) - Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
- Replace bootstrap caret with fontawesome caret (ClemMakesApps)
- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533 - Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
- Add organization field to user profile - Add organization field to user profile
- Fix deploy status responsiveness error !6633
- Fix resolved discussion display in side-by-side diff view !6575 - Fix resolved discussion display in side-by-side diff view !6575
- Optimize GitHub importing for speed and memory - Optimize GitHub importing for speed and memory
- API: expose pipeline data in builds API (!6502, Guilherme Salazar) - API: expose pipeline data in builds API (!6502, Guilherme Salazar)
...@@ -74,6 +87,7 @@ v 8.12.4 ...@@ -74,6 +87,7 @@ v 8.12.4
- Fix failed project deletion when feature visibility set to private. !6688 - Fix failed project deletion when feature visibility set to private. !6688
- Prevent claiming associated model IDs via import. - Prevent claiming associated model IDs via import.
- Set GitLab project exported file permissions to owner only - Set GitLab project exported file permissions to owner only
- Change user & group landing page routing from /u/:username to /:username
v 8.12.3 v 8.12.3
- Update Gitlab Shell to support low IO priority for storage moves - Update Gitlab Shell to support low IO priority for storage moves
......
...@@ -120,6 +120,7 @@ gem 'creole', '~> 0.5.0' ...@@ -120,6 +120,7 @@ gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1' gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2' gem 'asciidoctor', '~> 1.5.2'
gem 'rouge', '~> 2.0' gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
......
...@@ -769,6 +769,9 @@ GEM ...@@ -769,6 +769,9 @@ GEM
tilt (2.0.5) tilt (2.0.5)
timecop (0.8.1) timecop (0.8.1)
timfel-krb5-auth (0.8.3) timfel-krb5-auth (0.8.3)
truncato (0.7.8)
htmlentities (~> 4.3.1)
nokogiri (~> 1.6.1)
turbolinks (2.5.3) turbolinks (2.5.3)
coffee-rails coffee-rails
tzinfo (1.2.2) tzinfo (1.2.2)
...@@ -1004,6 +1007,7 @@ DEPENDENCIES ...@@ -1004,6 +1007,7 @@ DEPENDENCIES
test_after_commit (~> 0.4.2) test_after_commit (~> 0.4.2)
thin (~> 1.7.0) thin (~> 1.7.0)
timecop (~> 0.8.0) timecop (~> 0.8.0)
truncato (~> 0.7.8)
turbolinks (~> 2.5.0) turbolinks (~> 2.5.0)
u2f (~> 0.2.1) u2f (~> 0.2.1)
uglifier (~> 2.7.2) uglifier (~> 2.7.2)
......
...@@ -21,16 +21,14 @@ ...@@ -21,16 +21,14 @@
}; };
Activities.prototype.toggleFilter = function(sender) { Activities.prototype.toggleFilter = function(sender) {
var event_filters, filter; var filter = sender.attr("id").split("_")[0];
$('.event-filter .active').removeClass("active"); $('.event-filter .active').removeClass("active");
event_filters = $.cookie("event_filter"); $.cookie("event_filter", filter, {
filter = sender.attr("id").split("_")[0];
$.cookie("event_filter", (event_filters !== filter ? filter : ""), {
path: gon.relative_url_root || '/' path: gon.relative_url_root || '/'
}); });
if (event_filters !== filter) {
return sender.closest('li').toggleClass("active"); sender.closest('li').toggleClass("active");
}
}; };
return Activities; return Activities;
......
...@@ -21,7 +21,8 @@ ...@@ -21,7 +21,8 @@
}, },
data () { data () {
return { return {
filters: Store.state.filters filters: Store.state.filters,
showIssueForm: false
}; };
}, },
watch: { watch: {
...@@ -33,6 +34,11 @@ ...@@ -33,6 +34,11 @@
deep: true deep: true
} }
}, },
methods: {
showNewIssueForm() {
this.showIssueForm = !this.showIssueForm;
}
},
ready () { ready () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({ const options = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled, disabled: this.disabled,
......
...@@ -8,10 +8,8 @@ ...@@ -8,10 +8,8 @@
data () { data () {
return { return {
predefinedLabels: [ predefinedLabels: [
new ListLabel({ title: 'Development', color: '#5CB85C' }), new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
new ListLabel({ title: 'Testing', color: '#F0AD4E' }), new ListLabel({ title: 'Doing', color: '#5CB85C' })
new ListLabel({ title: 'Production', color: '#FF5F00' }),
new ListLabel({ title: 'Ready', color: '#FF0000' })
] ]
} }
}, },
......
//= require ./board_card //= require ./board_card
//= require ./board_new_issue
(() => { (() => {
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
...@@ -8,14 +9,16 @@ ...@@ -8,14 +9,16 @@
gl.issueBoards.BoardList = Vue.extend({ gl.issueBoards.BoardList = Vue.extend({
components: { components: {
'board-card': gl.issueBoards.BoardCard 'board-card': gl.issueBoards.BoardCard,
'board-new-issue': gl.issueBoards.BoardNewIssue
}, },
props: { props: {
disabled: Boolean, disabled: Boolean,
list: Object, list: Object,
issues: Array, issues: Array,
loading: Boolean, loading: Boolean,
issueLinkBase: String issueLinkBase: String,
showIssueForm: Boolean
}, },
data () { data () {
return { return {
...@@ -73,7 +76,7 @@ ...@@ -73,7 +76,7 @@
group: 'issues', group: 'issues',
sort: false, sort: false,
disabled: this.disabled, disabled: this.disabled,
filter: '.board-list-count', filter: '.board-list-count, .is-disabled',
onStart: (e) => { onStart: (e) => {
const card = this.$refs.issue[e.oldIndex]; const card = this.$refs.issue[e.oldIndex];
......
(() => {
window.gl = window.gl || {};
gl.issueBoards.BoardNewIssue = Vue.extend({
props: {
list: Object,
showIssueForm: Boolean
},
data() {
return {
title: '',
error: false
};
},
watch: {
showIssueForm () {
this.$els.input.focus();
}
},
methods: {
submit(e) {
e.preventDefault();
if (this.title.trim() === '') return;
this.error = false;
const labels = this.list.label ? [this.list.label] : [];
const issue = new ListIssue({
title: this.title,
labels
});
this.list.newIssue(issue)
.then((data) => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$els.submitButton).enable();
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$els.submitButton).enable();
// Remove the issue
this.list.removeIssue(issue);
// Show error message
this.error = true;
this.showIssueForm = true;
});
this.cancel();
},
cancel() {
this.showIssueForm = false;
this.title = '';
}
}
});
})();
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
fallbackClass: 'is-dragging', fallbackClass: 'is-dragging',
fallbackOnBody: true, fallbackOnBody: true,
ghostClass: 'is-ghost', ghostClass: 'is-ghost',
filter: '.has-tooltip', filter: '.has-tooltip, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 0, delay: gl.issueBoards.touchEnabled ? 100 : 0,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20, scrollSpeed: 20,
......
...@@ -87,6 +87,17 @@ class List { ...@@ -87,6 +87,17 @@ class List {
}); });
} }
newIssue (issue) {
this.addIssue(issue);
this.issuesSize++;
return gl.boardService.newIssue(this.id, issue)
.then((resp) => {
const data = resp.json();
issue.id = data.iid;
});
}
createIssues (data) { createIssues (data) {
data.forEach((issueObj) => { data.forEach((issueObj) => {
this.addIssue(new ListIssue(issueObj)); this.addIssue(new ListIssue(issueObj));
......
...@@ -58,4 +58,10 @@ class BoardService { ...@@ -58,4 +58,10 @@ class BoardService {
to_list_id to_list_id
}); });
} }
newIssue (id, issue) {
return this.issues.save({ id }, {
issue
});
}
}; };
...@@ -41,6 +41,11 @@ ...@@ -41,6 +41,11 @@
gl.utils.getPagePath = function() { gl.utils.getPagePath = function() {
return $('body').data('page').split(':')[0]; return $('body').data('page').split(':')[0];
}; };
gl.utils.parseUrl = function (url) {
var parser = document.createElement('a');
parser.href = url;
return parser;
};
return jQuery.timefor = function(time, suffix, expiredLabel) { return jQuery.timefor = function(time, suffix, expiredLabel) {
var suffixFromNow, timefor; var suffixFromNow, timefor;
if (!time) { if (!time) {
......
...@@ -61,6 +61,9 @@ ...@@ -61,6 +61,9 @@
function MergeRequestTabs(opts) { function MergeRequestTabs(opts) {
this.opts = opts != null ? opts : {}; this.opts = opts != null ? opts : {};
this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true; this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
this.buildsLoaded = this.opts.buildsLoaded || false;
this.setCurrentAction = bind(this.setCurrentAction, this); this.setCurrentAction = bind(this.setCurrentAction, this);
this.tabShown = bind(this.tabShown, this); this.tabShown = bind(this.tabShown, this);
this.showTab = bind(this.showTab, this); this.showTab = bind(this.showTab, this);
...@@ -93,7 +96,7 @@ ...@@ -93,7 +96,7 @@
this.loadCommits($target.attr('href')); this.loadCommits($target.attr('href'));
this.expandView(); this.expandView();
this.resetViewContainer(); this.resetViewContainer();
} else if (action === 'diffs') { } else if (this.isDiffAction(action)) {
this.loadDiff($target.attr('href')); this.loadDiff($target.attr('href'));
if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') { if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
this.shrinkView(); this.shrinkView();
...@@ -170,8 +173,9 @@ ...@@ -170,8 +173,9 @@
action = 'notes'; action = 'notes';
} }
this.currentAction = action; this.currentAction = action;
// Remove a trailing '/commits' or '/diffs' // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, ''); new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
// Append the new action if we're on a tab other than 'notes' // Append the new action if we're on a tab other than 'notes'
if (action !== 'notes') { if (action !== 'notes') {
new_state += "/" + action; new_state += "/" + action;
...@@ -210,8 +214,13 @@ ...@@ -210,8 +214,13 @@
if (this.diffsLoaded) { if (this.diffsLoaded) {
return; return;
} }
// We extract pathname for the current Changes tab anchor href
// some pages like MergeRequestsController#new has query parameters on that anchor
var url = gl.utils.parseUrl(source);
return this._get({ return this._get({
url: (source + ".json") + this._location.search, url: (url.pathname + ".json") + this._location.search,
success: (function(_this) { success: (function(_this) {
return function(data) { return function(data) {
$('#diffs').html(data.html); $('#diffs').html(data.html);
...@@ -223,7 +232,7 @@ ...@@ -223,7 +232,7 @@
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
$('#diffs .js-syntax-highlight').syntaxHighlight(); $('#diffs .js-syntax-highlight').syntaxHighlight();
$('#diffs .diff-file').singleFileDiff(); $('#diffs .diff-file').singleFileDiff();
if (_this.diffViewType() === 'parallel' && _this.currentAction === 'diffs') { if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) {
_this.expandViewContainer(); _this.expandViewContainer();
} }
_this.diffsLoaded = true; _this.diffsLoaded = true;
...@@ -324,6 +333,10 @@ ...@@ -324,6 +333,10 @@
return $('.inline-parallel-buttons a.active').data('view-type'); return $('.inline-parallel-buttons a.active').data('view-type');
}; };
MergeRequestTabs.prototype.isDiffAction = function(action) {
return action === 'diffs' || action === 'new/diffs'
};
MergeRequestTabs.prototype.expandViewContainer = function() { MergeRequestTabs.prototype.expandViewContainer = function() {
var $wrapper = $('.content-wrapper .container-fluid'); var $wrapper = $('.content-wrapper .container-fluid');
if (this.fixedLayoutPref === null) { if (this.fixedLayoutPref === null) {
......
...@@ -3,12 +3,21 @@ ...@@ -3,12 +3,21 @@
const $pipelineBtn = $(this).closest('.toggle-pipeline-btn'); const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph'); const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
const $btnText = $(this).find('.toggle-btn-text'); const $btnText = $(this).find('.toggle-btn-text');
const $icon = $(this).find('.fa');
$($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed'); $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed'); const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
const expandIcon = 'fa-caret-down';
const hideIcon = 'fa-caret-up';
graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide') if(graphCollapsed) {
$btnText.text('Expand');
$icon.removeClass(hideIcon).addClass(expandIcon);
} else {
$btnText.text('Hide');
$icon.removeClass(expandIcon).addClass(hideIcon);
}
} }
$(document).on('click', '.toggle-pipeline-btn', toggleGraph); $(document).on('click', '.toggle-pipeline-btn', toggleGraph);
......
...@@ -89,7 +89,7 @@ content on the Users#show page. ...@@ -89,7 +89,7 @@ content on the Users#show page.
const action = $target.data('action'); const action = $target.data('action');
const source = $target.attr('href'); const source = $target.attr('href');
this.setTab(source, action); this.setTab(source, action);
return this.setCurrentAction(action); return this.setCurrentAction(source, action);
} }
activateTab(action) { activateTab(action) {
...@@ -142,14 +142,9 @@ content on the Users#show page. ...@@ -142,14 +142,9 @@ content on the Users#show page.
.toggle(status); .toggle(status);
} }
setCurrentAction(action) { setCurrentAction(source, action) {
const regExp = new RegExp(`\/(${this.actions.join('|')})(\.html)?\/?$`); let new_state = source
let new_state = this._location.pathname;
new_state = new_state.replace(/\/+$/, ''); new_state = new_state.replace(/\/+$/, '');
new_state = new_state.replace(regExp, '');
if (action !== this.defaultAction) {
new_state += `/${action}`;
}
new_state += this._location.search + this._location.hash; new_state += this._location.search + this._location.hash;
history.replaceState({ history.replaceState({
turbolinks: true, turbolinks: true,
......
...@@ -71,8 +71,8 @@ ...@@ -71,8 +71,8 @@
return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
}); });
}; };
collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/u/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>'); collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/u/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>'); assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
return $dropdown.glDropdown({ return $dropdown.glDropdown({
showMenuAbove: showMenuAbove, showMenuAbove: showMenuAbove,
data: function(term, callback) { data: function(term, callback) {
......
...@@ -194,10 +194,17 @@ ...@@ -194,10 +194,17 @@
pointer-events: none !important; pointer-events: none !important;
} }
.caret { .fa-caret-down,
.fa-caret-up {
margin-left: 5px; margin-left: 5px;
} }
&.dropdown-toggle {
.fa-caret-down {
margin-left: 3px;
}
}
svg { svg {
height: 15px; height: 15px;
width: 15px; width: 15px;
......
.caret {
display: inline-block;
width: 0;
height: 0;
margin-left: 2px;
vertical-align: middle;
border-top: $caret-width-base dashed;
border-right: $caret-width-base solid transparent;
border-left: $caret-width-base solid transparent;
}
.btn-group {
.caret {
margin-left: 0;
}
}
.dropdown { .dropdown {
position: relative; position: relative;
......
...@@ -81,10 +81,10 @@ label { ...@@ -81,10 +81,10 @@ label {
.select-wrapper { .select-wrapper {
position: relative; position: relative;
.caret { .fa-caret-down {
position: absolute; position: absolute;
right: 10px; right: 10px;
top: $gl-padding; top: 10px;
color: $gray-darkest; color: $gray-darkest;
pointer-events: none; pointer-events: none;
} }
......
...@@ -57,6 +57,10 @@ header { ...@@ -57,6 +57,10 @@ header {
&:hover, &:focus, &:active { &:hover, &:focus, &:active {
background-color: $background-color; background-color: $background-color;
} }
.fa-caret-down {
font-size: 15px;
}
} }
.navbar-toggle { .navbar-toggle {
......
...@@ -21,7 +21,14 @@ ...@@ -21,7 +21,14 @@
padding-right: 10px; padding-right: 10px;
b { b {
@extend .caret; display: inline-block;
width: 0;
height: 0;
margin-left: 2px;
vertical-align: middle;
border-top: $caret-width-base dashed;
border-right: $caret-width-base solid transparent;
border-left: $caret-width-base solid transparent;
color: $gray-darkest; color: $gray-darkest;
} }
} }
......
...@@ -162,6 +162,10 @@ lex ...@@ -162,6 +162,10 @@ lex
list-style: none; list-style: none;
overflow-y: scroll; overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
&.is-smaller {
height: calc(100% - 185px);
}
} }
.board-list-loading { .board-list-loading {
...@@ -233,3 +237,31 @@ lex ...@@ -233,3 +237,31 @@ lex
margin-right: 5px; margin-right: 5px;
} }
} }
.board-new-issue-form {
margin: 5px;
}
.board-issue-count-holder {
margin-top: -3px;
.btn {
line-height: 12px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
.board-issue-count {
padding-right: 10px;
padding-left: 10px;
line-height: 21px;
border-radius: $border-radius-base;
border: 1px solid $border-color;
&.has-btn {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-width: 1px 0 1px 1px;
}
}
...@@ -229,9 +229,12 @@ ...@@ -229,9 +229,12 @@
.fa { .fa {
color: $table-text-gray; color: $table-text-gray;
margin-right: 6px;
font-size: 14px; font-size: 14px;
} }
svg, .fa {
margin-right: 0;
}
} }
.btn-remove { .btn-remove {
...@@ -272,18 +275,8 @@ ...@@ -272,18 +275,8 @@
.toggle-pipeline-btn { .toggle-pipeline-btn {
background-color: $gray-dark; background-color: $gray-dark;
.caret {
border-top: none;
border-bottom: 4px solid;
}
&.graph-collapsed { &.graph-collapsed {
background-color: $white-light; background-color: $white-light;
.caret {
border-bottom: none;
border-top: 4px solid;
}
} }
} }
......
...@@ -94,7 +94,7 @@ ...@@ -94,7 +94,7 @@
.profile-user-bio { .profile-user-bio {
// Limits the width of the user bio for readability. // Limits the width of the user bio for readability.
max-width: 600px; max-width: 600px;
margin: 15px auto 0; margin: 10px auto;
padding: 0 16px; padding: 0 16px;
} }
...@@ -213,29 +213,22 @@ ...@@ -213,29 +213,22 @@
} }
.user-profile { .user-profile {
.cover-controls a { .cover-controls a {
margin-left: 5px; margin-left: 5px;
} }
.profile-header { .profile-header {
margin: 0 auto; margin: 0 auto;
.avatar-holder { .avatar-holder {
width: 90px; width: 90px;
display: inline-block; margin: 0 auto 10px;
}
.user-info {
display: inline-block;
text-align: left;
vertical-align: middle;
margin-left: 15px;
.handle {
color: $gl-gray-light;
}
.member-date {
margin-bottom: 4px;
}
} }
} }
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.cover-block { .cover-block {
padding-top: 20px; padding-top: 20px;
} }
...@@ -258,10 +251,6 @@ ...@@ -258,10 +251,6 @@
} }
} }
.user-profile-nav {
margin-top: 15px;
}
table.u2f-registrations { table.u2f-registrations {
th:not(:last-child), td:not(:last-child) { th:not(:last-child), td:not(:last-child) {
border-right: solid 1px transparent; border-right: solid 1px transparent;
......
...@@ -37,7 +37,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController ...@@ -37,7 +37,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
end end
def preview def preview
@message = broadcast_message_params[:message] @broadcast_message = BroadcastMessage.new(broadcast_message_params)
end end
protected protected
......
...@@ -177,7 +177,8 @@ class ApplicationController < ActionController::Base ...@@ -177,7 +177,8 @@ class ApplicationController < ActionController::Base
end end
def event_filter def event_filter
filters = cookies['event_filter'].split(',') if cookies['event_filter'].present? # Split using comma to maintain backward compatibility Ex/ "filter1,filter2"
filters = cookies['event_filter'].split(',')[0] if cookies['event_filter'].present?
@event_filter ||= EventFilter.new(filters) @event_filter ||= EventFilter.new(filters)
end end
......
...@@ -2,6 +2,7 @@ module Projects ...@@ -2,6 +2,7 @@ module Projects
module Boards module Boards
class IssuesController < Boards::ApplicationController class IssuesController < Boards::ApplicationController
before_action :authorize_read_issue!, only: [:index] before_action :authorize_read_issue!, only: [:index]
before_action :authorize_create_issue!, only: [:create]
before_action :authorize_update_issue!, only: [:update] before_action :authorize_update_issue!, only: [:update]
def index def index
...@@ -9,16 +10,23 @@ module Projects ...@@ -9,16 +10,23 @@ module Projects
issues = issues.page(params[:page]) issues = issues.page(params[:page])
render json: { render json: {
issues: issues.as_json( issues: serialize_as_json(issues),
only: [:iid, :title, :confidential],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
}),
size: issues.total_count size: issues.total_count
} }
end end
def create
list = project.board.lists.find(params[:list_id])
service = ::Boards::Issues::CreateService.new(project, current_user, issue_params)
issue = service.execute(list)
if issue.valid?
render json: serialize_as_json(issue)
else
render json: issue.errors, status: :unprocessable_entity
end
end
def update def update
service = ::Boards::Issues::MoveService.new(project, current_user, move_params) service = ::Boards::Issues::MoveService.new(project, current_user, move_params)
...@@ -43,6 +51,10 @@ module Projects ...@@ -43,6 +51,10 @@ module Projects
return render_403 unless can?(current_user, :read_issue, project) return render_403 unless can?(current_user, :read_issue, project)
end end
def authorize_create_issue!
return render_403 unless can?(current_user, :admin_issue, project)
end
def authorize_update_issue! def authorize_update_issue!
return render_403 unless can?(current_user, :update_issue, issue) return render_403 unless can?(current_user, :update_issue, issue)
end end
...@@ -54,6 +66,19 @@ module Projects ...@@ -54,6 +66,19 @@ module Projects
def move_params def move_params
params.permit(:id, :from_list_id, :to_list_id) params.permit(:id, :from_list_id, :to_list_id)
end end
def issue_params
params.require(:issue).permit(:title).merge(request: request)
end
def serialize_as_json(resource)
resource.as_json(
only: [:iid, :title, :confidential],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
})
end
end end
end end
end end
...@@ -165,7 +165,8 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -165,7 +165,8 @@ class Projects::IssuesController < Projects::ApplicationController
protected protected
def issue def issue
@noteable = @issue ||= @project.issues.find_by(iid: params[:id]) || redirect_old # The Sortable default scope causes performance issues when used with find_by
@noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old
end end
alias_method :subscribable_resource, :issue alias_method :subscribable_resource, :issue
alias_method :issuable, :issue alias_method :issuable, :issue
......
...@@ -20,6 +20,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -20,6 +20,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :define_diff_comment_vars, only: [:diffs] before_action :define_diff_comment_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines]
before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines] before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
before_action :apply_diff_view_cookie!, only: [:new_diffs]
before_action :build_merge_request, only: [:new, :new_diffs]
# Allow read any merge_request # Allow read any merge_request
before_action :authorize_read_merge_request! before_action :authorize_read_merge_request!
...@@ -211,33 +213,29 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -211,33 +213,29 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
def new def new
apply_diff_view_cookie! define_new_vars
build_merge_request
@noteable = @merge_request
@target_branches = if @merge_request.target_project
@merge_request.target_project.repository.branch_names
else
[]
end
@target_project = merge_request.target_project
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
@diffs = @merge_request.diffs(diff_options) if @merge_request.compare
@diff_notes_disabled = true
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses.relevant if @pipeline
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
set_suggested_approvers set_suggested_approvers
end end
def new_diffs
respond_to do |format|
format.html do
define_new_vars
render "new"
end
format.json do
@diffs = if @merge_request.can_be_created
@merge_request.diffs(diff_options)
else
[]
end
@diff_notes_disabled = true
render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) }
end
end
end
def create def create
@target_branches ||= [] @target_branches ||= []
create_params = clamp_approvals_before_merge(merge_request_params) create_params = clamp_approvals_before_merge(merge_request_params)
...@@ -530,6 +528,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -530,6 +528,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController
) )
end end
def define_new_vars
@noteable = @merge_request
@target_branches = if @merge_request.target_project
@merge_request.target_project.repository.branch_names
else
[]
end
@target_project = merge_request.target_project
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses.relevant if @pipeline
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
end
def invalid_mr def invalid_mr
# Render special view for MR with removed target branch # Render special view for MR with removed target branch
render 'invalid' render 'invalid'
...@@ -580,7 +599,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -580,7 +599,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def build_merge_request def build_merge_request
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute
end end
def compared_diff_version def compared_diff_version
......
...@@ -16,7 +16,7 @@ module AppearancesHelper ...@@ -16,7 +16,7 @@ module AppearancesHelper
end end
def brand_text def brand_text
markdown(brand_item.description) markdown_field(brand_item, :description)
end end
def brand_item def brand_item
......
...@@ -11,22 +11,6 @@ module ApplicationSettingsHelper ...@@ -11,22 +11,6 @@ module ApplicationSettingsHelper
current_application_settings.signin_enabled? current_application_settings.signin_enabled?
end end
def extra_sign_in_text
current_application_settings.sign_in_text
end
def help_text
current_application_settings.help_text
end
def after_sign_up_text
current_application_settings.after_sign_up_text
end
def shared_runners_text
current_application_settings.shared_runners_text
end
def user_oauth_applications? def user_oauth_applications?
current_application_settings.user_oauth_applications current_application_settings.user_oauth_applications
end end
......
...@@ -3,7 +3,7 @@ module BroadcastMessagesHelper ...@@ -3,7 +3,7 @@ module BroadcastMessagesHelper
return unless message.present? return unless message.present?
content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do
icon('bullhorn') << ' ' << render_broadcast_message(message.message) icon('bullhorn') << ' ' << render_broadcast_message(message)
end end
end end
...@@ -32,7 +32,7 @@ module BroadcastMessagesHelper ...@@ -32,7 +32,7 @@ module BroadcastMessagesHelper
end end
end end
def render_broadcast_message(message) def render_broadcast_message(broadcast_message)
Banzai.render(message, pipeline: :broadcast_message).html_safe Banzai.render_field(broadcast_message, :message).html_safe
end end
end end
...@@ -13,14 +13,12 @@ module GitlabMarkdownHelper ...@@ -13,14 +13,12 @@ module GitlabMarkdownHelper
def link_to_gfm(body, url, html_options = {}) def link_to_gfm(body, url, html_options = {})
return "" if body.blank? return "" if body.blank?
escaped_body = if body.start_with?('<img') context = {
body project: @project,
else current_user: (current_user if defined?(current_user)),
escape_once(body) pipeline: :single_line,
end }
gfm_body = Banzai.render(body, context)
user = current_user if defined?(current_user)
gfm_body = Banzai.render(escaped_body, project: @project, current_user: user, pipeline: :single_line)
fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body) fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
if fragment.children.size == 1 && fragment.children[0].name == 'a' if fragment.children.size == 1 && fragment.children[0].name == 'a'
...@@ -51,17 +49,15 @@ module GitlabMarkdownHelper ...@@ -51,17 +49,15 @@ module GitlabMarkdownHelper
context[:project] ||= @project context[:project] ||= @project
html = Banzai.render(text, context) html = Banzai.render(text, context)
banzai_postprocess(html, context)
end
context.merge!( def markdown_field(object, field)
current_user: (current_user if defined?(current_user)), object = object.for_display if object.respond_to?(:for_display)
return "" unless object.present?
# RelativeLinkFilter
requested_path: @path,
project_wiki: @project_wiki,
ref: @ref
)
Banzai.post_process(html, context) html = Banzai.render_field(object, field)
banzai_postprocess(html, object.banzai_render_context(field))
end end
def asciidoc(text) def asciidoc(text)
...@@ -196,4 +192,18 @@ module GitlabMarkdownHelper ...@@ -196,4 +192,18 @@ module GitlabMarkdownHelper
icon(options[:icon]) icon(options[:icon])
end end
end end
# Calls Banzai.post_process with some common context options
def banzai_postprocess(html, context)
context.merge!(
current_user: (current_user if defined?(current_user)),
# RelativeLinkFilter
requested_path: @path,
project_wiki: @project_wiki,
ref: @ref
)
Banzai.post_process(html, context)
end
end end
...@@ -208,8 +208,18 @@ module SearchHelper ...@@ -208,8 +208,18 @@ module SearchHelper
search_path(options) search_path(options)
end end
# Sanitize html generated after parsing markdown from issue description or comment # Sanitize a HTML field for search display. Most tags are stripped out and the
def search_md_sanitize(html) # maximum length is set to 200 characters.
def search_md_sanitize(object, field)
html = markdown_field(object, field)
html = Truncato.truncate(
html,
count_tags: false,
count_tail: false,
max_length: 200
)
# Truncato's filtered_tags and filtered_attributes are not quite the same
sanitize(html, tags: %w(a p ol ul li pre code)) sanitize(html, tags: %w(a p ol ul li pre code))
end end
end end
class AbuseReport < ActiveRecord::Base class AbuseReport < ActiveRecord::Base
include CacheMarkdownField
cache_markdown_field :message, pipeline: :single_line
belongs_to :reporter, class_name: 'User' belongs_to :reporter, class_name: 'User'
belongs_to :user belongs_to :user
...@@ -7,6 +11,9 @@ class AbuseReport < ActiveRecord::Base ...@@ -7,6 +11,9 @@ class AbuseReport < ActiveRecord::Base
validates :message, presence: true validates :message, presence: true
validates :user_id, uniqueness: { message: 'has already been reported' } validates :user_id, uniqueness: { message: 'has already been reported' }
# For CacheMarkdownField
alias_method :author, :reporter
def remove_user(deleted_by:) def remove_user(deleted_by:)
user.block user.block
DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true) DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true)
......
class Appearance < ActiveRecord::Base class Appearance < ActiveRecord::Base
include CacheMarkdownField
cache_markdown_field :description
validates :title, presence: true validates :title, presence: true
validates :description, presence: true validates :description, presence: true
validates :logo, file_size: { maximum: 1.megabyte } validates :logo, file_size: { maximum: 1.megabyte }
......
class ApplicationSetting < ActiveRecord::Base class ApplicationSetting < ActiveRecord::Base
include CacheMarkdownField
include TokenAuthenticatable include TokenAuthenticatable
add_authentication_token_field :runners_registration_token add_authentication_token_field :runners_registration_token
add_authentication_token_field :health_check_access_token add_authentication_token_field :health_check_access_token
...@@ -17,6 +19,11 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -17,6 +19,11 @@ class ApplicationSetting < ActiveRecord::Base
serialize :domain_whitelist, Array serialize :domain_whitelist, Array
serialize :domain_blacklist, Array serialize :domain_blacklist, Array
cache_markdown_field :sign_in_text
cache_markdown_field :help_page_text
cache_markdown_field :shared_runners_text, pipeline: :plain_markdown
cache_markdown_field :after_sign_up_text
attr_accessor :domain_whitelist_raw, :domain_blacklist_raw attr_accessor :domain_whitelist_raw, :domain_blacklist_raw
validates :session_expire_delay, validates :session_expire_delay,
......
class BroadcastMessage < ActiveRecord::Base class BroadcastMessage < ActiveRecord::Base
include CacheMarkdownField
include Sortable include Sortable
cache_markdown_field :message, pipeline: :broadcast_message
validates :message, presence: true validates :message, presence: true
validates :starts_at, presence: true validates :starts_at, presence: true
validates :ends_at, presence: true validates :ends_at, presence: true
......
...@@ -251,9 +251,8 @@ module Ci ...@@ -251,9 +251,8 @@ module Ci
Ci::ProcessPipelineService.new(project, user).execute(self) Ci::ProcessPipelineService.new(project, user).execute(self)
end end
def build_updated def update_status
with_lock do with_lock do
reload
case latest_builds_status case latest_builds_status
when 'pending' then enqueue when 'pending' then enqueue
when 'running' then run when 'running' then run
......
...@@ -84,13 +84,18 @@ class CommitStatus < ActiveRecord::Base ...@@ -84,13 +84,18 @@ class CommitStatus < ActiveRecord::Base
commit_status.update_attributes finished_at: Time.now commit_status.update_attributes finished_at: Time.now
end end
after_transition any => [:success, :failed, :canceled] do |commit_status|
commit_status.pipeline.try(:process!)
true
end
after_transition do |commit_status, transition| after_transition do |commit_status, transition|
commit_status.pipeline.try(:build_updated) unless transition.loopback? commit_status.pipeline.try do |pipeline|
break if transition.loopback?
if commit_status.complete?
ProcessPipelineWorker.perform_async(pipeline.id)
end
UpdatePipelineWorker.perform_async(pipeline.id)
end
true
end end
after_transition [:created, :pending, :running] => :success do |commit_status| after_transition [:created, :pending, :running] => :success do |commit_status|
......
# This module takes care of updating cache columns for Markdown-containing
# fields. Use like this in the body of your class:
#
# include CacheMarkdownField
# cache_markdown_field :foo
# cache_markdown_field :bar
# cache_markdown_field :baz, pipeline: :single_line
#
# Corresponding foo_html, bar_html and baz_html fields should exist.
module CacheMarkdownField
# Knows about the relationship between markdown and html field names, and
# stores the rendering contexts for the latter
class FieldData
extend Forwardable
def initialize
@data = {}
end
def_delegators :@data, :[], :[]=
def_delegator :@data, :keys, :markdown_fields
def html_field(markdown_field)
"#{markdown_field}_html"
end
def html_fields
markdown_fields.map {|field| html_field(field) }
end
end
# Dynamic registries don't really work in Rails as it's not guaranteed that
# every class will be loaded, so hardcode the list.
CACHING_CLASSES = %w[
AbuseReport
Appearance
ApplicationSetting
BroadcastMessage
Issue
Label
MergeRequest
Milestone
Namespace
Note
Project
Release
Snippet
]
def self.caching_classes
CACHING_CLASSES.map(&:constantize)
end
extend ActiveSupport::Concern
included do
cattr_reader :cached_markdown_fields do
FieldData.new
end
# Returns the default Banzai render context for the cached markdown field.
def banzai_render_context(field)
raise ArgumentError.new("Unknown field: #{field.inspect}") unless
cached_markdown_fields.markdown_fields.include?(field)
# Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project)
context = cached_markdown_fields[field].merge(project: project)
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
context
end
# Allow callers to look up the cache field name, rather than hardcoding it
def markdown_cache_field_for(field)
raise ArgumentError.new("Unknown field: #{field}") unless
cached_markdown_fields.markdown_fields.include?(field)
cached_markdown_fields.html_field(field)
end
# Always exclude _html fields from attributes (including serialization).
# They contain unredacted HTML, which would be a security issue
alias_method :attributes_before_markdown_cache, :attributes
def attributes
attrs = attributes_before_markdown_cache
cached_markdown_fields.html_fields.each do |field|
attrs.delete(field)
end
attrs
end
end
class_methods do
private
# Specify that a field is markdown. Its rendered output will be cached in
# a corresponding _html field. Any custom rendering options may be provided
# as a context.
def cache_markdown_field(markdown_field, context = {})
raise "Add #{self} to CacheMarkdownField::CACHING_CLASSES" unless
CacheMarkdownField::CACHING_CLASSES.include?(self.to_s)
cached_markdown_fields[markdown_field] = context
html_field = cached_markdown_fields.html_field(markdown_field)
cache_method = "#{markdown_field}_cache_refresh".to_sym
invalidation_method = "#{html_field}_invalidated?".to_sym
define_method(cache_method) do
html = Banzai::Renderer.cacheless_render_field(self, markdown_field)
__send__("#{html_field}=", html)
true
end
# The HTML becomes invalid if any dependent fields change. For now, assume
# author and project invalidate the cache in all circumstances.
define_method(invalidation_method) do
changed_fields = changed_attributes.keys
invalidations = changed_fields & [markdown_field.to_s, "author", "project"]
!invalidations.empty?
end
before_save cache_method, if: invalidation_method
end
end
end
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
# #
module Issuable module Issuable
extend ActiveSupport::Concern extend ActiveSupport::Concern
include CacheMarkdownField
include Participable include Participable
include Mentionable include Mentionable
include Subscribable include Subscribable
...@@ -13,6 +14,9 @@ module Issuable ...@@ -13,6 +14,9 @@ module Issuable
include Awardable include Awardable
included do included do
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
belongs_to :assignee, class_name: "User" belongs_to :assignee, class_name: "User"
belongs_to :updated_by, class_name: "User" belongs_to :updated_by, class_name: "User"
......
...@@ -4,6 +4,10 @@ class GlobalLabel ...@@ -4,6 +4,10 @@ class GlobalLabel
delegate :color, :description, to: :@first_label delegate :color, :description, to: :@first_label
def for_display
@first_label
end
def self.build_collection(labels) def self.build_collection(labels)
labels = labels.group_by(&:title) labels = labels.group_by(&:title)
......
...@@ -4,6 +4,10 @@ class GlobalMilestone ...@@ -4,6 +4,10 @@ class GlobalMilestone
attr_accessor :title, :milestones attr_accessor :title, :milestones
alias_attribute :name, :title alias_attribute :name, :title
def for_display
@first_milestone
end
def self.build_collection(milestones) def self.build_collection(milestones)
milestones = milestones.group_by(&:title) milestones = milestones.group_by(&:title)
...@@ -17,6 +21,7 @@ class GlobalMilestone ...@@ -17,6 +21,7 @@ class GlobalMilestone
@title = title @title = title
@name = title @name = title
@milestones = milestones @milestones = milestones
@first_milestone = milestones.find {|m| m.description.present? } || milestones.first
end end
def safe_title def safe_title
......
class Label < ActiveRecord::Base class Label < ActiveRecord::Base
include CacheMarkdownField
include Referable include Referable
include Subscribable include Subscribable
...@@ -8,6 +9,8 @@ class Label < ActiveRecord::Base ...@@ -8,6 +9,8 @@ class Label < ActiveRecord::Base
None = LabelStruct.new('No Label', 'No Label') None = LabelStruct.new('No Label', 'No Label')
Any = LabelStruct.new('Any Label', '') Any = LabelStruct.new('Any Label', '')
cache_markdown_field :description, pipeline: :single_line
DEFAULT_COLOR = '#428BCA' DEFAULT_COLOR = '#428BCA'
default_value_for :color, DEFAULT_COLOR default_value_for :color, DEFAULT_COLOR
......
...@@ -34,7 +34,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -34,7 +34,7 @@ class MergeRequest < ActiveRecord::Base
# Temporary fields to store compare vars # Temporary fields to store compare vars
# when creating new merge request # when creating new merge request
attr_accessor :can_be_created, :compare_commits, :compare attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
state_machine :state, initial: :opened do state_machine :state, initial: :opened do
event :close do event :close do
...@@ -201,7 +201,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -201,7 +201,7 @@ class MergeRequest < ActiveRecord::Base
end end
def diff_size def diff_size
merge_request_diff.size diffs(diff_options).size
end end
def diff_base_commit def diff_base_commit
......
...@@ -6,6 +6,7 @@ class Milestone < ActiveRecord::Base ...@@ -6,6 +6,7 @@ class Milestone < ActiveRecord::Base
Any = MilestoneStruct.new('Any Milestone', '', -1) Any = MilestoneStruct.new('Any Milestone', '', -1)
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2) Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
include CacheMarkdownField
include InternalId include InternalId
include Sortable include Sortable
include Referable include Referable
...@@ -13,6 +14,9 @@ class Milestone < ActiveRecord::Base ...@@ -13,6 +14,9 @@ class Milestone < ActiveRecord::Base
include Elastic::MilestonesSearch include Elastic::MilestonesSearch
include Milestoneish include Milestoneish
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
belongs_to :project belongs_to :project
has_many :issues has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
......
class Namespace < ActiveRecord::Base class Namespace < ActiveRecord::Base
acts_as_paranoid acts_as_paranoid
include CacheMarkdownField
include Sortable include Sortable
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
cache_markdown_field :description, pipeline: :description
has_many :projects, dependent: :destroy has_many :projects, dependent: :destroy
belongs_to :owner, class_name: "User" belongs_to :owner, class_name: "User"
......
...@@ -7,10 +7,13 @@ class Note < ActiveRecord::Base ...@@ -7,10 +7,13 @@ class Note < ActiveRecord::Base
include Awardable include Awardable
include Importable include Importable
include FasterCacheKeys include FasterCacheKeys
include CacheMarkdownField
cache_markdown_field :note, pipeline: :note
# Attribute containing rendered and redacted Markdown as generated by # Attribute containing rendered and redacted Markdown as generated by
# Banzai::ObjectRenderer. # Banzai::ObjectRenderer.
attr_accessor :note_html attr_accessor :redacted_note_html
# An Array containing the number of visible references as generated by # An Array containing the number of visible references as generated by
# Banzai::ObjectRenderer # Banzai::ObjectRenderer
......
...@@ -6,6 +6,7 @@ class Project < ActiveRecord::Base ...@@ -6,6 +6,7 @@ class Project < ActiveRecord::Base
include Gitlab::VisibilityLevel include Gitlab::VisibilityLevel
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
include AccessRequestable include AccessRequestable
include CacheMarkdownField
include Referable include Referable
include Sortable include Sortable
include AfterCommitQueue include AfterCommitQueue
...@@ -18,6 +19,8 @@ class Project < ActiveRecord::Base ...@@ -18,6 +19,8 @@ class Project < ActiveRecord::Base
UNKNOWN_IMPORT_URL = 'http://unknown.git' UNKNOWN_IMPORT_URL = 'http://unknown.git'
cache_markdown_field :description, pipeline: :description
delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true
default_value_for :archived, false default_value_for :archived, false
......
class Release < ActiveRecord::Base class Release < ActiveRecord::Base
include CacheMarkdownField
cache_markdown_field :description
belongs_to :project belongs_to :project
validates :description, :project, :tag, presence: true validates :description, :project, :tag, presence: true
......
...@@ -922,6 +922,52 @@ class Repository ...@@ -922,6 +922,52 @@ class Repository
end end
end end
def multi_action(user:, branch:, message:, actions:, author_email: nil, author_name: nil)
update_branch_with_hooks(user, branch) do |ref|
index = rugged.index
parents = []
branch = find_branch(ref)
if branch
last_commit = branch.target
index.read_tree(last_commit.raw_commit.tree)
parents = [last_commit.sha]
end
actions.each do |action|
case action[:action]
when :create, :update, :move
mode =
case action[:action]
when :update
index.get(action[:file_path])[:mode]
when :move
index.get(action[:previous_path])[:mode]
end
mode ||= 0o100644
index.remove(action[:previous_path]) if action[:action] == :move
content = action[:encoding] == 'base64' ? Base64.decode64(action[:content]) : action[:content]
oid = rugged.write(content, :blob)
index.add(path: action[:file_path], oid: oid, mode: mode)
when :delete
index.remove(action[:file_path])
end
end
options = {
tree: index.write_tree(rugged),
message: message,
parents: parents
}
options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
Rugged::Commit.create(rugged, options)
end
end
def get_committer_and_author(user, email: nil, name: nil) def get_committer_and_author(user, email: nil, name: nil)
committer = user_to_committer(user) committer = user_to_committer(user)
author = Gitlab::Git::committer_hash(email: email, name: name) || committer author = Gitlab::Git::committer_hash(email: email, name: name) || committer
......
class Snippet < ActiveRecord::Base class Snippet < ActiveRecord::Base
include Gitlab::VisibilityLevel include Gitlab::VisibilityLevel
include Linguist::BlobHelper include Linguist::BlobHelper
include CacheMarkdownField
include Participable include Participable
include Referable include Referable
include Sortable include Sortable
include Elastic::SnippetsSearch include Elastic::SnippetsSearch
include Awardable include Awardable
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :content
# If file_name changes, it invalidates content
alias_method :default_content_html_invalidator, :content_html_invalidated?
def content_html_invalidated?
default_content_html_invalidator || file_name_changed?
end
default_value_for :visibility_level, Snippet::PRIVATE default_value_for :visibility_level, Snippet::PRIVATE
belongs_to :author, class_name: 'User' belongs_to :author, class_name: 'User'
......
...@@ -56,9 +56,8 @@ class BaseService ...@@ -56,9 +56,8 @@ class BaseService
result result
end end
def success def success(pass_back = {})
{ pass_back[:status] = :success
status: :success pass_back
}
end end
end end
module Boards
module Issues
class CreateService < Boards::BaseService
def execute(list)
params.merge!(label_ids: [list.label_id])
create_issue
end
private
def create_issue
::Issues::CreateService.new(project, current_user, params).execute
end
end
end
end
...@@ -36,12 +36,7 @@ module Boards ...@@ -36,12 +36,7 @@ module Boards
end end
def set_state def set_state
params[:state] = params[:state] = list.done? ? 'closed' : 'opened'
case list.list_type.to_sym
when :backlog then 'opened'
when :done then 'closed'
else 'all'
end
end end
def board_label_ids def board_label_ids
......
...@@ -25,10 +25,8 @@ module Boards ...@@ -25,10 +25,8 @@ module Boards
def label_params def label_params
[ [
{ name: 'Development', color: '#5CB85C' }, { name: 'To Do', color: '#F0AD4E' },
{ name: 'Testing', color: '#F0AD4E' }, { name: 'Doing', color: '#5CB85C' }
{ name: 'Production', color: '#FF5F00' },
{ name: 'Ready', color: '#FF0000' }
] ]
end end
end end
......
...@@ -27,8 +27,9 @@ module Files ...@@ -27,8 +27,9 @@ module Files
create_target_branch create_target_branch
end end
if commit result = commit
success if result
success(result: result)
else else
error('Something went wrong. Your changes were not committed') error('Something went wrong. Your changes were not committed')
end end
...@@ -42,6 +43,12 @@ module Files ...@@ -42,6 +43,12 @@ module Files
@source_branch != @target_branch || @source_project != @project @source_branch != @target_branch || @source_project != @project
end end
def file_has_changed?
return false unless @last_commit_sha && last_commit
@last_commit_sha != last_commit.sha
end
def raise_error(message) def raise_error(message)
raise ValidationError.new(message) raise ValidationError.new(message)
end end
......
require_relative "base_service"
module Files
class MultiService < Files::BaseService
class FileChangedError < StandardError; end
def commit
repository.multi_action(
user: current_user,
branch: @target_branch,
message: @commit_message,
actions: params[:actions],
author_email: @author_email,
author_name: @author_name
)
end
private
def validate
super
params[:actions].each_with_index do |action, index|
unless action[:file_path].present?
raise_error("You must specify a file_path.")
end
regex_check(action[:file_path])
regex_check(action[:previous_path]) if action[:previous_path]
if project.empty_repo? && action[:action] != :create
raise_error("No files to #{action[:action]}.")
end
validate_file_exists(action)
case action[:action]
when :create
validate_create(action)
when :update
validate_update(action)
when :delete
validate_delete(action)
when :move
validate_move(action, index)
else
raise_error("Unknown action type `#{action[:action]}`.")
end
end
end
def validate_file_exists(action)
return if action[:action] == :create
file_path = action[:file_path]
file_path = action[:previous_path] if action[:action] == :move
blob = repository.blob_at_branch(params[:branch_name], file_path)
unless blob
raise_error("File to be #{action[:action]}d `#{file_path}` does not exist.")
end
end
def last_commit
Gitlab::Git::Commit.last_for_path(repository, @source_branch, @file_path)
end
def regex_check(file)
if file =~ Gitlab::Regex.directory_traversal_regex
raise_error(
'Your changes could not be committed, because the file name, `' +
file +
'` ' +
Gitlab::Regex.directory_traversal_regex_message
)
end
unless file =~ Gitlab::Regex.file_path_regex
raise_error(
'Your changes could not be committed, because the file name, `' +
file +
'` ' +
Gitlab::Regex.file_path_regex_message
)
end
end
def validate_create(action)
return if project.empty_repo?
if repository.blob_at_branch(params[:branch_name], action[:file_path])
raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.")
end
end
def validate_delete(action)
end
def validate_move(action, index)
if action[:previous_path].nil?
raise_error("You must supply the original file path when moving file `#{action[:file_path]}`.")
end
blob = repository.blob_at_branch(params[:branch_name], action[:file_path])
if blob
raise_error("Move destination `#{action[:file_path]}` already exists.")
end
if action[:content].nil?
blob = repository.blob_at_branch(params[:branch_name], action[:previous_path])
blob.load_all_data!(repository) if blob.truncated?
params[:actions][index][:content] = blob.data
end
end
def validate_update(action)
if file_has_changed?
raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.")
end
end
end
end
...@@ -23,12 +23,6 @@ module Files ...@@ -23,12 +23,6 @@ module Files
end end
end end
def file_has_changed?
return false unless @last_commit_sha && last_commit
@last_commit_sha != last_commit.sha
end
def last_commit def last_commit
@last_commit ||= Gitlab::Git::Commit. @last_commit ||= Gitlab::Git::Commit.
last_for_path(@source_project.repository, @source_branch, @file_path) last_for_path(@source_project.repository, @source_branch, @file_path)
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
%td %td
%strong.subheading.visible-xs-block.visible-sm-block Message %strong.subheading.visible-xs-block.visible-sm-block Message
.message .message
= markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter) = markdown_field(abuse_report, :message)
%td %td
- if user - if user
= link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true), = link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true),
......
.broadcast-message-preview{ style: broadcast_message_style(@broadcast_message) } .broadcast-message-preview{ style: broadcast_message_style(@broadcast_message) }
= icon('bullhorn') = icon('bullhorn')
.js-broadcast-message-preview .js-broadcast-message-preview
= render_broadcast_message(@broadcast_message.message.presence || "Your message here") - if @broadcast_message.message.present?
= render_broadcast_message(@broadcast_message)
- else
= "Your message here"
= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f| = form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f|
= form_errors(@broadcast_message) = form_errors(@broadcast_message)
......
$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@message))}"); $('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@broadcast_message))}");
...@@ -23,4 +23,4 @@ ...@@ -23,4 +23,4 @@
- if group.description.present? - if group.description.present?
.description .description
= markdown(group.description, pipeline: :description) = markdown_field(group, :description)
%li{id: dom_id(label)} %li{id: dom_id(label)}
.label-row .label-row
= render_colored_label(label, tooltip: false) = render_colored_label(label, tooltip: false)
= markdown(label.description, pipeline: :single_line) = markdown_field(label, :description)
.pull-right .pull-right
= link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm' = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm'
= link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"} = link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"}
...@@ -87,7 +87,7 @@ ...@@ -87,7 +87,7 @@
- if project.description.present? - if project.description.present?
.description .description
= markdown(project.description, pipeline: :description) = markdown_field(project, :description)
= paginate @projects, theme: 'gitlab' = paginate @projects, theme: 'gitlab'
- else - else
......
...@@ -55,7 +55,7 @@ ...@@ -55,7 +55,7 @@
= sort_options_hash[@sort] = sort_options_hash[@sort]
- else - else
= sort_title_recently_created = sort_title_recently_created
%b.caret = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
%li %li
= link_to todos_filter_path(sort: sort_value_priority) do = link_to todos_filter_path(sort: sort_value_priority) do
......
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
Almost there... Almost there...
%p.lead %p.lead
Please check your email to confirm your account Please check your email to confirm your account
- if after_sign_up_text.present? - if current_application_settings.after_sign_up_text.present?
.well-confirmation.text-center .well-confirmation.text-center
= markdown(after_sign_up_text) = markdown_field(current_application_settings, :after_sign_up_text)
%p.confirmation-content.text-center %p.confirmation-content.text-center
No confirmation email received? Please check your spam folder or No confirmation email received? Please check your spam folder or
.append-bottom-20.prepend-top-20.text-center .append-bottom-20.prepend-top-20.text-center
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
= sort_options_hash[@sort] = sort_options_hash[@sort]
- else - else
= sort_title_recently_created = sort_title_recently_created
%b.caret = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
%li %li
= link_to explore_groups_path(sort: sort_value_recently_created) do = link_to explore_groups_path(sort: sort_value_recently_created) do
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
= visibility_level_label(params[:visibility_level].to_i) = visibility_level_label(params[:visibility_level].to_i)
- else - else
Any Any
%b.caret = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
%li %li
= link_to filter_projects_path(visibility_level: nil) do = link_to filter_projects_path(visibility_level: nil) do
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
= params[:tag] = params[:tag]
- else - else
Any Any
%b.caret = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
%li %li
= link_to filter_projects_path(tag: nil) do = link_to filter_projects_path(tag: nil) do
......
...@@ -33,8 +33,8 @@ ...@@ -33,8 +33,8 @@
.form-group .form-group
= f.label :projects, "Projects", class: "control-label" = f.label :projects, "Projects", class: "control-label"
.col-sm-10 .col-sm-10
= f.collection_select :project_ids, @group.projects, :id, :name, = f.collection_select :project_ids, @group.projects.non_archived, :id, :name,
{ selected: @group.projects.map(&:id) }, multiple: true, class: 'select2' { selected: @group.projects.non_archived.pluck(:id) }, multiple: true, class: 'select2'
.col-md-6 .col-md-6
.form-group .form-group
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
- if @group.description.present? - if @group.description.present?
.cover-desc.description .cover-desc.description
= markdown(@group.description, pipeline: :description) = markdown_field(@group, :description)
%div.groups-header{ class: container_class } %div.groups-header{ class: container_class }
.top-area .top-area
......
...@@ -9,10 +9,10 @@ ...@@ -9,10 +9,10 @@
= version_status_badge = version_status_badge
%br %br
Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}. Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}.
- if help_text.present? - if current_application_settings.help_text.present?
%hr %hr
%p.slead %p.slead
= markdown(help_text) = markdown(current_application_settings.help_text)
- else - else
%p.slead %p.slead
GitLab is open source software to collaborate on code. GitLab is open source software to collaborate on code.
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
- if current_application_settings.help_page_text.present? - if current_application_settings.help_page_text.present?
%hr %hr
= markdown(current_application_settings.help_page_text) = markdown_field(current_application_settings, :help_page_text)
%hr %hr
......
...@@ -25,13 +25,13 @@ ...@@ -25,13 +25,13 @@
Perform code reviews and enhance collaboration with merge requests. Perform code reviews and enhance collaboration with merge requests.
Each project can also have an issue tracker and a wiki. Each project can also have an issue tracker and a wiki.
- if extra_sign_in_text.present? - if current_application_settings.sign_in_text.present?
= markdown(extra_sign_in_text) = markdown_field(current_application_settings, :sign_in_text)
- if help_text.present? - if current_application_settings.help_text.present?
%h3 Need help? %h3 Need help?
%hr %hr
%p.slead %p.slead
= markdown(help_text) = markdown(current_application_settings.help_text)
%hr %hr
.container .container
......
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
%li.header-user.dropdown %li.header-user.dropdown
= link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do = link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do
= image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar" = image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar"
%span.caret = icon('caret-down')
.dropdown-menu-nav.dropdown-menu-align-right .dropdown-menu-nav.dropdown-menu-align-right
%ul %ul
%li %li
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
.project-home-desc .project-home-desc
- if @project.description.present? - if @project.description.present?
= markdown(@project.description, pipeline: :description) = markdown_field(@project, :description)
- if forked_from_project = @project.forked_from_project - if forked_from_project = @project.forked_from_project
%p %p
......
...@@ -12,8 +12,17 @@ ...@@ -12,8 +12,17 @@
%header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" } %header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
%h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" } %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
{{ list.title }} {{ list.title }}
%span.pull-right{ "v-if" => "list.type !== 'blank'" } .board-issue-count-holder.pull-right.clearfix{ "v-if" => "list.type !== 'blank'" }
{{ list.issuesSize }} %span.board-issue-count.pull-left{ ":class" => "{ 'has-btn': list.type !== 'done' }" }
{{ list.issuesSize }}
- if can?(current_user, :admin_issue, @project)
%button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
"@click" => "showNewIssueForm",
"v-if" => "list.type !== 'done'",
"aria-label" => "Add an issue",
"title" => "Add an issue",
data: { placement: "top", container: "body" } }
= icon("plus")
- if can?(current_user, :admin_list, @project) - if can?(current_user, :admin_list, @project)
%board-delete{ "inline-template" => true, %board-delete{ "inline-template" => true,
":list" => "list", ":list" => "list",
...@@ -26,12 +35,38 @@ ...@@ -26,12 +35,38 @@
":issues" => "list.issues", ":issues" => "list.issues",
":loading" => "list.loading", ":loading" => "list.loading",
":disabled" => "disabled", ":disabled" => "disabled",
":show-issue-form.sync" => "showIssueForm",
":issue-link-base" => "issueLinkBase" } ":issue-link-base" => "issueLinkBase" }
.board-list-loading.text-center{ "v-if" => "loading" } .board-list-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin") = icon("spinner spin")
- if can? current_user, :create_issue, @project
%board-new-issue{ "inline-template" => true,
":list" => "list",
":show-issue-form.sync" => "showIssueForm",
"v-show" => "list.type !== 'done' && showIssueForm" }
.card.board-new-issue-form
%form{ "@submit" => "submit($event)" }
.flash-container{ "v-if" => "error" }
.flash-alert
An error occured. Please try again.
%label.label-light{ ":for" => "list.id + '-title'" }
Title
%input.form-control{ type: "text",
"v-model" => "title",
"v-el:input" => true,
":id" => "list.id + '-title'" }
.clearfix.prepend-top-10
%button.btn.btn-success.pull-left{ type: "submit",
":disabled" => "title === ''",
"v-el:submit-button" => true }
Submit issue
%button.btn.btn-default.pull-right{ type: "button",
"@click" => "cancel" }
Cancel
%ul.board-list{ "v-el:list" => true, %ul.board-list{ "v-el:list" => true,
"v-show" => "!loading", "v-show" => "!loading",
":data-board" => "list.id" } ":data-board" => "list.id",
":class" => "{ 'is-smaller': showIssueForm }" }
= render "projects/boards/components/card" = render "projects/boards/components/card"
%li.board-list-count.text-center{ "v-if" => "showCount" } %li.board-list-count.text-center{ "v-if" => "showCount" }
= icon("spinner spin", "v-show" => "list.loadingMore" ) = icon("spinner spin", "v-show" => "list.loadingMore" )
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
":issue-link-base" => "issueLinkBase", ":issue-link-base" => "issueLinkBase",
":disabled" => "disabled", ":disabled" => "disabled",
"track-by" => "id" } "track-by" => "id" }
%li.card{ ":class" => "{ 'user-can-drag': !disabled }", %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id }",
":index" => "index" } ":index" => "index" }
%h4.card-title %h4.card-title
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential") = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
":title" => "issue.title" } ":title" => "issue.title" }
{{ issue.title }} {{ issue.title }}
.card-footer .card-footer
%span.card-number %span.card-number{ "v-if" => "issue.id" }
= precede '#' do = precede '#' do
{{ issue.id }} {{ issue.id }}
%button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels", %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
":title" => "label.description", ":title" => "label.description",
data: { container: 'body' } } data: { container: 'body' } }
{{ label.title }} {{ label.title }}
%a.has-tooltip{ ":href" => "'/u/' + issue.assignee.username", %a.has-tooltip{ ":href" => "'/' + issue.assignee.username",
":title" => "'Assigned to ' + issue.assignee.name", ":title" => "'Assigned to ' + issue.assignee.name",
"v-if" => "issue.assignee", "v-if" => "issue.assignee",
data: { container: 'body' } } data: { container: 'body' } }
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
%span.light %span.light
= projects_sort_options_hash[@sort] = projects_sort_options_hash[@sort]
%b.caret = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
%li %li
= link_to filter_branches_path(sort: sort_value_name) do = link_to filter_branches_path(sort: sort_value_name) do
......
...@@ -67,7 +67,7 @@ ...@@ -67,7 +67,7 @@
.btn-group .btn-group
%a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
= custom_icon('icon_play') = custom_icon('icon_play')
%b.caret = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |build| - actions.each do |build|
%li %li
...@@ -78,7 +78,7 @@ ...@@ -78,7 +78,7 @@
.btn-group .btn-group
%a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'} %a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'}
= icon("download") = icon("download")
%b.caret = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
- artifacts.each do |build| - artifacts.each do |build|
%li %li
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
.dropdown.inline .dropdown.inline
%a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } } %a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } }
%span.hidden-xs Options %span.hidden-xs Options
%span.caret.commit-options-dropdown-caret = icon('caret-down', class: ".commit-options-dropdown-caret")
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
%li.visible-xs-block.visible-sm-block %li.visible-xs-block.visible-sm-block
= link_to namespace_project_tree_path(@project.namespace, @project, @commit) do = link_to namespace_project_tree_path(@project.namespace, @project, @commit) do
...@@ -24,6 +24,8 @@ ...@@ -24,6 +24,8 @@
= revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
%li.clearfix %li.clearfix
= cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
%li.clearfix
= link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit)
%li.divider %li.divider
%li.dropdown-header %li.dropdown-header
Download Download
...@@ -63,10 +65,10 @@ ...@@ -63,10 +65,10 @@
.commit-box.content-block .commit-box.content-block
%h3.commit-title %h3.commit-title
= markdown escape_once(@commit.title), pipeline: :single_line, author: @commit.author = markdown(@commit.title, pipeline: :single_line, author: @commit.author)
- if @commit.description.present? - if @commit.description.present?
%pre.commit-description %pre.commit-description
= preserve(markdown(escape_once(@commit.description), pipeline: :single_line, author: @commit.author)) = preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author))
:javascript :javascript
$(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}"); $(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}");
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.btn.btn-grouped.btn-white.toggle-pipeline-btn .btn.btn-grouped.btn-white.toggle-pipeline-btn
%span.toggle-btn-text Hide %span.toggle-btn-text Hide
%span pipeline graph %span pipeline graph
%span.caret = icon('caret-up')
- if can?(current_user, :update_pipeline, pipeline.project) - if can?(current_user, :update_pipeline, pipeline.project)
- if pipeline.builds.latest.failed.any?(&:retryable?) - if pipeline.builds.latest.failed.any?(&:retryable?)
= link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
- if commit.description? - if commit.description?
%pre.commit-row-description.js-toggle-content %pre.commit-row-description.js-toggle-content
= preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author)) = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
.commit-row-info .commit-row-info
= commit_author_link(commit, avatar: false, size: 24) = commit_author_link(commit, avatar: false, size: 24)
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
.dropdown .dropdown
%a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
= custom_icon('icon_play') = custom_icon('icon_play')
%b.caret = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |action| - actions.each do |action|
%li %li
......
- show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true) - show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true)
- can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project)
- diff_files = diffs.diff_files - diff_files = diffs.diff_files
.content-block.oneline-block.files-changed .content-block.oneline-block.files-changed
...@@ -20,7 +21,7 @@ ...@@ -20,7 +21,7 @@
- if diff_files.overflow? - if diff_files.overflow?
= render 'projects/diffs/warning', diff_files: diff_files = render 'projects/diffs/warning', diff_files: diff_files
.files{data: {can_create_note: (!@diff_notes_disabled && can?(current_user, :create_note, diffs.project))}} .files{ data: { can_create_note: can_create_note } }
- diff_files.each_with_index do |diff_file, index| - diff_files.each_with_index do |diff_file, index|
- diff_commit = commit_for_diff(diff_file) - diff_commit = commit_for_diff(diff_file)
- blob = diff_file.blob(diff_commit) - blob = diff_file.blob(diff_commit)
......
...@@ -109,9 +109,12 @@ ...@@ -109,9 +109,12 @@
%strong Container Registry %strong Container Registry
%br %br
%span.descr Enable Container Registry for this repository %span.descr Enable Container Registry for this repository
= link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank'
%hr %hr
= render 'issues_settings', f: f = render 'issues_settings', f: f
%hr %hr
= render 'merge_request_settings', f: f = render 'merge_request_settings', f: f
%hr %hr
%fieldset.features.append-bottom-default %fieldset.features.append-bottom-default
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
= sort_options_hash[@sort] = sort_options_hash[@sort]
- else - else
= sort_title_recently_created = sort_title_recently_created
%b.caret = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
%li %li
- excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id] - excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id]
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
= label_tag :link_group_access, "Max access level", class: "label-light" = label_tag :link_group_access, "Max access level", class: "label-light"
.select-wrapper .select-wrapper
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
%span.caret = icon('caret-down')
.form-group .form-group
= label_tag :expires_at, 'Access expiration date', class: 'label-light' = label_tag :expires_at, 'Access expiration date', class: 'label-light'
.clearable-input .clearable-input
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
.issuable-actions .issuable-actions
.clearfix.issue-btn-group.dropdown .clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
%span.caret = icon('caret-down')
Options Options
.dropdown-menu.dropdown-menu-align-right.hidden-lg .dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul %ul
...@@ -55,12 +55,12 @@ ...@@ -55,12 +55,12 @@
.issue-details.issuable-details .issue-details.issuable-details
.detail-page-description.content-block .detail-page-description.content-block
%h2.title %h2.title
= markdown escape_once(@issue.title), pipeline: :single_line, author: @issue.author = markdown_field(@issue, :title)
- if @issue.description.present? - if @issue.description.present?
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' } .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
.wiki .wiki
= preserve do = preserve do
= markdown(@issue.description, cache_key: [@issue, "description"], author: @issue.author) = markdown_field(@issue, :description)
%textarea.hidden.js-task-list-field %textarea.hidden.js-task-list-field
= @issue.description = @issue.description
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
.visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown
%button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } } %button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } }
Options Options
%span.caret = icon('caret-down')
.dropdown-menu.dropdown-menu-align-right .dropdown-menu.dropdown-menu-align-right
%ul %ul
%li %li
......
= render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false
...@@ -19,34 +19,32 @@ ...@@ -19,34 +19,32 @@
.mr-compare.merge-request .mr-compare.merge-request
%ul.merge-request-tabs.nav-links.no-top.no-bottom %ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.commits-tab %li.commits-tab.active
= link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
Commits Commits
%span.badge= @commits.size %span.badge= @commits.size
- if @pipeline - if @pipeline
%li.builds-tab.active %li.builds-tab
= link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do
Builds Builds
%span.badge= @statuses.size %span.badge= @statuses.size
%li.diffs-tab.active %li.diffs-tab
= link_to url_for(params), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do = link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do
Changes Changes
%span.badge= @diffs.real_size %span.badge= @merge_request.diff_size
.tab-content .tab-content
#commits.commits.tab-pane #commits.commits.tab-pane.active
= render "projects/merge_requests/show/commits" = render "projects/merge_requests/show/commits"
#diffs.diffs.tab-pane.active #diffs.diffs.tab-pane
- if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE - # This tab is always loaded via AJAX
.alert.alert-danger
%h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits.
%p To preserve performance the line changes are not shown.
- else
= render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false
- if @pipeline - if @pipeline
#builds.builds.tab-pane #builds.builds.tab-pane
= render "projects/merge_requests/show/builds" = render "projects/merge_requests/show/builds"
.mr-loading-status
= spinner
:javascript :javascript
$('.assign-to-me-link').on('click', function(e){ $('.assign-to-me-link').on('click', function(e){
$('#merge_request_assignee_id').val("#{current_user.id}").trigger("change"); $('#merge_request_assignee_id').val("#{current_user.id}").trigger("change");
...@@ -54,6 +52,6 @@ ...@@ -54,6 +52,6 @@
}); });
:javascript :javascript
var merge_request = new MergeRequest({ var merge_request = new MergeRequest({
action: "#{(@show_changes_tab ? 'diffs' : 'new')}", action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}",
setUrl: false buildsLoaded: "#{@pipeline ? 'true' : 'false'}"
}); });
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
%span.dropdown.inline.prepend-left-5 %span.dropdown.inline.prepend-left-5
%a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} } %a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} }
Download as Download as
%span.caret = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
%li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch)
%li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff)
......
.detail-page-description.content-block .detail-page-description.content-block
%h2.title %h2.title
= markdown escape_once(@merge_request.title), pipeline: :single_line, author: @merge_request.author = markdown_field(@merge_request, :title)
%div %div
- if @merge_request.description.present? - if @merge_request.description.present?
.description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''} .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''}
.wiki .wiki
= preserve do = preserve do
= markdown(@merge_request.description, cache_key: [@merge_request, "description"], author: @merge_request.author) = markdown_field(@merge_request, :description)
%textarea.hidden.js-task-list-field %textarea.hidden.js-task-list-field
= @merge_request.description = @merge_request.description
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
.issuable-actions .issuable-actions
.clearfix.issue-btn-group.dropdown .clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
%span.caret = icon('caret-down')
Options Options
.dropdown-menu.dropdown-menu-align-right.hidden-lg .dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul %ul
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
latest version latest version
- else - else
version #{version_index(@merge_request_diff)} version #{version_index(@merge_request_diff)}
%span.caret = icon('caret-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title .dropdown-title
%span Version: %span Version:
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
version #{version_index(@start_version)} version #{version_index(@start_version)}
- else - else
#{@merge_request.target_branch} #{@merge_request.target_branch}
%span.caret = icon('caret-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title .dropdown-title
%span Compared with: %span Compared with:
......
...@@ -49,11 +49,12 @@ ...@@ -49,11 +49,12 @@
.mr-widget-heading .mr-widget-heading
.ci_widget.ci-success .ci_widget.ci-success
= ci_icon_for_status("success") = ci_icon_for_status("success")
%span.hidden-sm %span
Deployed to Deployed to
= succeed '.' do = succeed '.' do
= link_to environment.name, environment_path(environment), class: 'environment' = link_to environment.name, environment_path(environment), class: 'environment'
- external_url = environment.external_url - external_url = environment.external_url
- if external_url - if external_url
= link_to external_url, target: '_blank' do = link_to external_url, target: '_blank' do
= icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true) %span.hidden-xs View on #{external_url.gsub(/\A.*?:\/\//, '')}
= icon('external-link', right: true)
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
Merge When Build Succeeds Merge When Build Succeeds
- unless @project.only_allow_merge_if_build_succeeds? - unless @project.only_allow_merge_if_build_succeeds?
= button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do
%span.caret = icon('caret-down')
%span.sr-only %span.sr-only
Select Merge Moment Select Merge Moment
%ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' } %ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' }
......
...@@ -30,13 +30,13 @@ ...@@ -30,13 +30,13 @@
.detail-page-description.milestone-detail .detail-page-description.milestone-detail
%h2.title %h2.title
= markdown escape_once(@milestone.title), pipeline: :single_line = markdown_field(@milestone, :title)
%div %div
- if @milestone.description.present? - if @milestone.description.present?
.description .description
.wiki .wiki
= preserve do = preserve do
= markdown @milestone.description = markdown_field(@milestone, :description)
- if @milestone.total_items_count(current_user).zero? - if @milestone.total_items_count(current_user).zero?
.alert.alert-success.prepend-top-default .alert.alert-success.prepend-top-default
......
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