Commit d1748c42 authored by Mike Greiling's avatar Mike Greiling

Merge branch 'ce-to-ee-2017-07-08' into 'master'

CE upstream: Pre-9.4 RC1

Closes #2890, #2878, #2716, gitlab-ce#34847, gitaly#355, #2839, and gitlab-ce#32568

See merge request !2390
parents 8dc2804b 469e970e
...@@ -399,7 +399,7 @@ gem 'sys-filesystem', '~> 1.1.6' ...@@ -399,7 +399,7 @@ gem 'sys-filesystem', '~> 1.1.6'
gem 'net-ntp' gem 'net-ntp'
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly', '~> 0.9.0' gem 'gitaly', '~> 0.14.0'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -302,7 +302,7 @@ GEM ...@@ -302,7 +302,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly (0.9.0) gitaly (0.14.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -1014,7 +1014,7 @@ DEPENDENCIES ...@@ -1014,7 +1014,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly (~> 0.9.0) gitaly (~> 0.14.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
......
import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = Object.assign({}, ISetter);
class CloseReopenReportToggle {
constructor(opts = {}) {
this.dropdownTrigger = opts.dropdownTrigger;
this.dropdownList = opts.dropdownList;
this.button = opts.button;
}
initDroplab() {
this.reopenItem = this.dropdownList.querySelector('.reopen-item');
this.closeItem = this.dropdownList.querySelector('.close-item');
this.droplab = new DropLab();
const config = this.setConfig();
this.droplab.init(this.dropdownTrigger, this.dropdownList, [InputSetter], config);
}
updateButton(isClosed) {
this.toggleButtonType(isClosed);
this.button.blur();
}
toggleButtonType(isClosed) {
const [showItem, hideItem] = this.getButtonTypes(isClosed);
showItem.classList.remove('hidden');
showItem.classList.add('droplab-item-selected');
hideItem.classList.add('hidden');
hideItem.classList.remove('droplab-item-selected');
showItem.click();
}
getButtonTypes(isClosed) {
return isClosed ? [this.reopenItem, this.closeItem] : [this.closeItem, this.reopenItem];
}
setDisable(shouldDisable = true) {
if (shouldDisable) {
this.button.setAttribute('disabled', 'true');
this.dropdownTrigger.setAttribute('disabled', 'true');
} else {
this.button.removeAttribute('disabled');
this.dropdownTrigger.removeAttribute('disabled');
}
}
setConfig() {
const config = {
InputSetter: [
{
input: this.button,
valueAttribute: 'data-text',
inputAttribute: 'data-value',
},
{
input: this.button,
valueAttribute: 'data-text',
inputAttribute: 'title',
},
{
input: this.button,
valueAttribute: 'data-button-class',
inputAttribute: 'class',
},
{
input: this.dropdownTrigger,
valueAttribute: 'data-toggle-class',
inputAttribute: 'class',
},
{
input: this.button,
valueAttribute: 'data-url',
inputAttribute: 'href',
},
{
input: this.button,
valueAttribute: 'data-method',
inputAttribute: 'data-method',
},
],
};
return config;
}
}
export default CloseReopenReportToggle;
import DropLab from './droplab/drop_lab'; import DropLab from './droplab/drop_lab';
import InputSetter from './droplab/plugins/input_setter'; import ISetter from './droplab/plugins/input_setter';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = Object.assign({}, ISetter);
class CommentTypeToggle { class CommentTypeToggle {
constructor(opts = {}) { constructor(opts = {}) {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import './lib/utils/url_utility'; import './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button'; import FilesCommentButton from './files_comment_button';
import SingleFileDiff from './single_file_diff';
const UNFOLD_COUNT = 20; const UNFOLD_COUNT = 20;
let isBound = false; let isBound = false;
...@@ -10,7 +11,11 @@ class Diff { ...@@ -10,7 +11,11 @@ class Diff {
constructor() { constructor() {
const $diffFile = $('.files .diff-file'); const $diffFile = $('.files .diff-file');
$diffFile.singleFileDiff(); $diffFile.each((index, file) => {
if (!$.data(file, 'singleFileDiff')) {
$.data(file, 'singleFileDiff', new SingleFileDiff(file));
}
});
FilesCommentButton.init($diffFile); FilesCommentButton.init($diffFile);
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
/* global UsernameValidator */
/* global ActiveTabMemoizer */
/* global ShortcutsNavigation */ /* global ShortcutsNavigation */
/* global IssuableIndex */ /* global IssuableIndex */
/* global ShortcutsIssuable */ /* global ShortcutsIssuable */
/* global ZenMode */
/* global Milestone */ /* global Milestone */
/* global IssuableForm */ /* global IssuableForm */
/* global LabelsSelect */ /* global LabelsSelect */
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global Commit */ /* global Commit */
/* global NotificationsForm */ /* global NotificationsForm */
/* global TreeView */
/* global NotificationsDropdown */ /* global NotificationsDropdown */
/* global GroupAvatar */ /* global GroupAvatar */
/* global LineHighlighter */ /* global LineHighlighter */
...@@ -25,7 +21,6 @@ ...@@ -25,7 +21,6 @@
/* global ProjectAvatar */ /* global ProjectAvatar */
/* global CompareAutocomplete */ /* global CompareAutocomplete */
/* global ProjectNew */ /* global ProjectNew */
/* global Star */
/* global ProjectShow */ /* global ProjectShow */
/* global Labels */ /* global Labels */
/* global Shortcuts */ /* global Shortcuts */
...@@ -57,6 +52,15 @@ import UsersSelect from './users_select'; ...@@ -57,6 +52,15 @@ import UsersSelect from './users_select';
import RefSelectDropdown from './ref_select_dropdown'; import RefSelectDropdown from './ref_select_dropdown';
import GfmAutoComplete from './gfm_auto_complete'; import GfmAutoComplete from './gfm_auto_complete';
import ShortcutsBlob from './shortcuts_blob'; import ShortcutsBlob from './shortcuts_blob';
import SigninTabsMemoizer from './signin_tabs_memoizer';
import Star from './star';
import Todos from './todos';
import TreeView from './tree';
import UsagePing from './usage_ping';
import UsernameValidator from './username_validator';
import VersionCheckImage from './version_check_image';
import Wikis from './wikis';
import ZenMode from './zen_mode';
import initSettingsPanels from './settings_panels'; import initSettingsPanels from './settings_panels';
import initExperimentalFlags from './experimental_flags'; import initExperimentalFlags from './experimental_flags';
import OAuthRememberMe from './oauth_remember_me'; import OAuthRememberMe from './oauth_remember_me';
...@@ -135,7 +139,7 @@ import AuditLogs from './audit_logs'; ...@@ -135,7 +139,7 @@ import AuditLogs from './audit_logs';
break; break;
case 'sessions:new': case 'sessions:new':
new UsernameValidator(); new UsernameValidator();
new ActiveTabMemoizer(); new SigninTabsMemoizer();
new OAuthRememberMe({ container: $(".omniauth-container") }).bindEvents(); new OAuthRememberMe({ container: $(".omniauth-container") }).bindEvents();
break; break;
case 'projects:boards:show': case 'projects:boards:show':
...@@ -171,7 +175,7 @@ import AuditLogs from './audit_logs'; ...@@ -171,7 +175,7 @@ import AuditLogs from './audit_logs';
new UsersSelect(); new UsersSelect();
break; break;
case 'dashboard:todos:index': case 'dashboard:todos:index':
new gl.Todos(); new Todos();
break; break;
case 'dashboard:projects:index': case 'dashboard:projects:index':
case 'dashboard:projects:starred': case 'dashboard:projects:starred':
...@@ -329,7 +333,7 @@ import AuditLogs from './audit_logs'; ...@@ -329,7 +333,7 @@ import AuditLogs from './audit_logs';
new gl.Members(); new gl.Members();
new UsersSelect(); new UsersSelect();
break; break;
case 'projects:settings:members:show': case 'projects:project_members:index':
new gl.MemberExpirationDate('.js-access-expiration-date-groups'); new gl.MemberExpirationDate('.js-access-expiration-date-groups');
new GroupsSelect(); new GroupsSelect();
new gl.MemberExpirationDate(); new gl.MemberExpirationDate();
...@@ -391,7 +395,7 @@ import AuditLogs from './audit_logs'; ...@@ -391,7 +395,7 @@ import AuditLogs from './audit_logs';
new BlobViewer(); new BlobViewer();
break; break;
case 'help:index': case 'help:index':
gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
break; break;
case 'search:show': case 'search:show':
new Search(); new Search();
...@@ -455,7 +459,7 @@ import AuditLogs from './audit_logs'; ...@@ -455,7 +459,7 @@ import AuditLogs from './audit_logs';
new Admin(); new Admin();
switch (path[1]) { switch (path[1]) {
case 'cohorts': case 'cohorts':
new gl.UsagePing(); new UsagePing();
break; break;
case 'groups': case 'groups':
new UsersSelect(); new UsersSelect();
...@@ -511,7 +515,7 @@ import AuditLogs from './audit_logs'; ...@@ -511,7 +515,7 @@ import AuditLogs from './audit_logs';
new NotificationsDropdown(); new NotificationsDropdown();
break; break;
case 'wikis': case 'wikis':
new gl.Wikis(); new Wikis();
shortcut_handler = new ShortcutsWiki(); shortcut_handler = new ShortcutsWiki();
new ZenMode(); new ZenMode();
new gl.GLForm($('.wiki-form'), true); new gl.GLForm($('.wiki-form'), true);
......
...@@ -30,6 +30,7 @@ class GfmAutoComplete { ...@@ -30,6 +30,7 @@ class GfmAutoComplete {
this.input.each((i, input) => { this.input.each((i, input) => {
const $input = $(input); const $input = $(input);
$input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
$input.on('change.atwho', () => input.dispatchEvent(new Event('input')));
// This triggers at.js again // This triggers at.js again
// Needed for quick actions with suffixes (ex: /label ~) // Needed for quick actions with suffixes (ex: /label ~)
$input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
......
import CloseReopenReportToggle from '../close_reopen_report_toggle';
function initCloseReopenReport() {
const container = document.querySelector('.js-issuable-close-dropdown');
if (!container) return undefined;
const dropdownTrigger = container.querySelector('.js-issuable-close-toggle');
const dropdownList = container.querySelector('.js-issuable-close-menu');
const button = container.querySelector('.js-issuable-close-button');
const closeReopenReportToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
button,
});
closeReopenReportToggle.initDroplab();
return closeReopenReportToggle;
}
const IssuablesHelper = {
initCloseReopenReport,
};
export default IssuablesHelper;
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
/* global GitLab */ /* global GitLab */
/* global ZenMode */
/* global Autosave */ /* global Autosave */
/* global GroupsSelect */ /* global GroupsSelect */
/* global dateFormat */ /* global dateFormat */
...@@ -8,6 +7,7 @@ ...@@ -8,6 +7,7 @@
import UsersSelect from './users_select'; import UsersSelect from './users_select';
import GfmAutoComplete from './gfm_auto_complete'; import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode';
(function() { (function() {
this.IssuableForm = (function() { this.IssuableForm = (function() {
......
...@@ -4,13 +4,14 @@ ...@@ -4,13 +4,14 @@
import 'vendor/jquery.waitforimages'; import 'vendor/jquery.waitforimages';
import '~/lib/utils/text_utility'; import '~/lib/utils/text_utility';
import './flash'; import './flash';
import './task_list'; import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown'; import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
class Issue { class Issue {
constructor() { constructor() {
if ($('a.btn-close').length) { if ($('a.btn-close').length) {
this.taskList = new gl.TaskList({ this.taskList = new TaskList({
dataType: 'issue', dataType: 'issue',
fieldName: 'description', fieldName: 'description',
selector: '.detail-page-description', selector: '.detail-page-description',
...@@ -28,6 +29,11 @@ class Issue { ...@@ -28,6 +29,11 @@ class Issue {
Issue.initMergeRequests(); Issue.initMergeRequests();
Issue.initRelatedBranches(); Issue.initRelatedBranches();
this.closeButtons = $('a.btn-close');
this.reopenButtons = $('a.btn-reopen');
this.initCloseReopenReport();
if (Issue.createMrDropdownWrap) { if (Issue.createMrDropdownWrap) {
this.createMergeRequestDropdown = new CreateMergeRequestDropdown(Issue.createMrDropdownWrap); this.createMergeRequestDropdown = new CreateMergeRequestDropdown(Issue.createMrDropdownWrap);
} }
...@@ -35,13 +41,8 @@ class Issue { ...@@ -35,13 +41,8 @@ class Issue {
initIssueBtnEventListeners() { initIssueBtnEventListeners() {
const issueFailMessage = 'Unable to update this issue at this time.'; const issueFailMessage = 'Unable to update this issue at this time.';
const closeButtons = $('a.btn-close');
const isClosedBadge = $('div.status-box-closed');
const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter');
const reopenButtons = $('a.btn-reopen');
return closeButtons.add(reopenButtons).on('click', (e) => { return $(document).on('click', 'a.btn-close, a.btn-reopen', (e) => {
var $button, shouldSubmit, url; var $button, shouldSubmit, url;
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
...@@ -50,7 +51,9 @@ class Issue { ...@@ -50,7 +51,9 @@ class Issue {
if (shouldSubmit) { if (shouldSubmit) {
Issue.submitNoteForm($button.closest('form')); Issue.submitNoteForm($button.closest('form'));
} }
$button.prop('disabled', true);
this.disableCloseReopenButton($button);
url = $button.attr('href'); url = $button.attr('href');
return $.ajax({ return $.ajax({
type: 'PUT', type: 'PUT',
...@@ -58,15 +61,19 @@ class Issue { ...@@ -58,15 +61,19 @@ class Issue {
}) })
.fail(() => new Flash(issueFailMessage)) .fail(() => new Flash(issueFailMessage))
.done((data) => { .done((data) => {
const isClosedBadge = $('div.status-box-closed');
const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter');
if ('id' in data) { if ('id' in data) {
$(document).trigger('issuable:change'); $(document).trigger('issuable:change');
const isClosed = $button.hasClass('btn-close'); const isClosed = $button.hasClass('btn-close');
closeButtons.toggleClass('hidden', isClosed);
reopenButtons.toggleClass('hidden', !isClosed);
isClosedBadge.toggleClass('hidden', !isClosed); isClosedBadge.toggleClass('hidden', !isClosed);
isOpenBadge.toggleClass('hidden', isClosed); isOpenBadge.toggleClass('hidden', isClosed);
this.toggleCloseReopenButton(isClosed);
let numProjectIssues = Number(projectIssuesCounter.text().replace(/[^\d]/, '')); let numProjectIssues = Number(projectIssuesCounter.text().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1; numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues)); projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues));
...@@ -83,12 +90,34 @@ class Issue { ...@@ -83,12 +90,34 @@ class Issue {
} else { } else {
new Flash(issueFailMessage); new Flash(issueFailMessage);
} }
})
$button.prop('disabled', false); .then(() => {
this.disableCloseReopenButton($button, false);
}); });
}); });
} }
initCloseReopenReport() {
this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport();
if (this.closeButtons) this.closeButtons = this.closeButtons.not('.issuable-close-button');
if (this.reopenButtons) this.reopenButtons = this.reopenButtons.not('.issuable-close-button');
}
disableCloseReopenButton($button, shouldDisable) {
if (this.closeReopenReportToggle) {
this.closeReopenReportToggle.setDisable(shouldDisable);
} else {
$button.prop('disabled', shouldDisable);
}
}
toggleCloseReopenButton(isClosed) {
if (this.closeReopenReportToggle) this.closeReopenReportToggle.updateButton(isClosed);
this.closeButtons.toggleClass('hidden', isClosed);
this.reopenButtons.toggleClass('hidden', !isClosed);
}
static submitNoteForm(form) { static submitNoteForm(form) {
var noteText; var noteText;
noteText = form.find("textarea.js-note-text").val(); noteText = form.find("textarea.js-note-text").val();
......
<script> <script>
import animateMixin from '../mixins/animate'; import animateMixin from '../mixins/animate';
import TaskList from '../../task_list';
export default { export default {
mixins: [animateMixin], mixins: [animateMixin],
...@@ -46,7 +47,7 @@ ...@@ -46,7 +47,7 @@
if (this.canUpdate) { if (this.canUpdate) {
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new gl.TaskList({ new TaskList({
dataType: 'issue', dataType: 'issue',
fieldName: 'description', fieldName: 'description',
selector: '.detail-page-description', selector: '.detail-page-description',
......
...@@ -143,26 +143,13 @@ import './render_math'; ...@@ -143,26 +143,13 @@ import './render_math';
import './right_sidebar'; import './right_sidebar';
import './search'; import './search';
import './search_autocomplete'; import './search_autocomplete';
import './signin_tabs_memoizer';
import './single_file_diff';
import './smart_interval'; import './smart_interval';
import './snippets_list'; import './snippets_list';
import './star'; import './star';
import './subscription'; import './subscription';
import './subscription_select'; import './subscription_select';
import './syntax_highlight'; import './syntax_highlight';
import './task_list';
import './todos';
import './tree';
import './usage_ping';
import './user'; import './user';
import './user_tabs';
import './username_validator';
import './users_select';
import './version_check_image';
import './visibility_select';
import './wikis';
import './zen_mode';
// EE-only scripts // EE-only scripts
import './admin_email_select'; import './admin_email_select';
......
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
/* global MergeRequestTabs */ /* global MergeRequestTabs */
import 'vendor/jquery.waitforimages'; import 'vendor/jquery.waitforimages';
import './task_list'; import TaskList from './task_list';
import './merge_request_tabs'; import './merge_request_tabs';
import IssuablesHelper from './helpers/issuables_helper';
(function() { (function() {
this.MergeRequest = (function() { this.MergeRequest = (function() {
...@@ -21,11 +22,14 @@ import './merge_request_tabs'; ...@@ -21,11 +22,14 @@ import './merge_request_tabs';
return _this.showAllCommits(); return _this.showAllCommits();
}; };
})(this)); })(this));
this.initTabs(); this.initTabs();
this.initMRBtnListeners(); this.initMRBtnListeners();
this.initCommitMessageListeners(); this.initCommitMessageListeners();
this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport();
if ($("a.btn-close").length) { if ($("a.btn-close").length) {
this.taskList = new gl.TaskList({ this.taskList = new TaskList({
dataType: 'merge_request', dataType: 'merge_request',
fieldName: 'description', fieldName: 'description',
selector: '.detail-page-description', selector: '.detail-page-description',
...@@ -64,11 +68,15 @@ import './merge_request_tabs'; ...@@ -64,11 +68,15 @@ import './merge_request_tabs';
if (shouldSubmit && $this.data('submitted')) { if (shouldSubmit && $this.data('submitted')) {
return; return;
} }
if (this.closeReopenReportToggle) this.closeReopenReportToggle.setDisable();
if (shouldSubmit) { if (shouldSubmit) {
if ($this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')) { if ($this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')) {
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
return _this.submitNoteForm($this.closest('form'), $this);
_this.submitNoteForm($this.closest('form'), $this);
} }
} }
}); });
......
...@@ -21,7 +21,7 @@ import CommentTypeToggle from './comment_type_toggle'; ...@@ -21,7 +21,7 @@ import CommentTypeToggle from './comment_type_toggle';
import loadAwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import './autosave'; import './autosave';
import './dropzone_input'; import './dropzone_input';
import './task_list'; import TaskList from './task_list';
window.autosize = autosize; window.autosize = autosize;
window.Dropzone = Dropzone; window.Dropzone = Dropzone;
...@@ -71,7 +71,7 @@ export default class Notes { ...@@ -71,7 +71,7 @@ export default class Notes {
this.addBinding(); this.addBinding();
this.setPollingInterval(); this.setPollingInterval();
this.setupMainTargetNoteForm(); this.setupMainTargetNoteForm();
this.taskList = new gl.TaskList({ this.taskList = new TaskList({
dataType: 'note', dataType: 'note',
fieldName: 'note', fieldName: 'note',
selector: '.notes' selector: '.notes'
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */
import VisibilitySelect from './visibility_select';
function highlightChanges($elm) { function highlightChanges($elm) {
$elm.addClass('highlight-changes'); $elm.addClass('highlight-changes');
setTimeout(() => $elm.removeClass('highlight-changes'), 10); setTimeout(() => $elm.removeClass('highlight-changes'), 10);
...@@ -36,7 +38,7 @@ function highlightChanges($elm) { ...@@ -36,7 +38,7 @@ function highlightChanges($elm) {
ProjectNew.prototype.initVisibilitySelect = function() { ProjectNew.prototype.initVisibilitySelect = function() {
const visibilityContainer = document.querySelector('.js-visibility-select'); const visibilityContainer = document.querySelector('.js-visibility-select');
if (!visibilityContainer) return; if (!visibilityContainer) return;
const visibilitySelect = new gl.VisibilitySelect(visibilityContainer); const visibilitySelect = new VisibilitySelect(visibilityContainer);
visibilitySelect.init(); visibilitySelect.init();
const $visibilitySelect = $(visibilityContainer).find('select'); const $visibilitySelect = $(visibilityContainer).find('select');
......
...@@ -6,7 +6,7 @@ import AccessorUtilities from './lib/utils/accessor'; ...@@ -6,7 +6,7 @@ import AccessorUtilities from './lib/utils/accessor';
* Memorize the last selected tab after reloading a page. * Memorize the last selected tab after reloading a page.
* Does that setting the current selected tab in the localStorage * Does that setting the current selected tab in the localStorage
*/ */
class ActiveTabMemoizer { export default class SigninTabsMemoizer {
constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) { constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) {
this.currentTabKey = currentTabKey; this.currentTabKey = currentTabKey;
this.tabSelector = tabSelector; this.tabSelector = tabSelector;
...@@ -51,5 +51,3 @@ class ActiveTabMemoizer { ...@@ -51,5 +51,3 @@ class ActiveTabMemoizer {
return window.localStorage.getItem(this.currentTabKey); return window.localStorage.getItem(this.currentTabKey);
} }
} }
window.ActiveTabMemoizer = ActiveTabMemoizer;
...@@ -2,18 +2,13 @@ ...@@ -2,18 +2,13 @@
import FilesCommentButton from './files_comment_button'; import FilesCommentButton from './files_comment_button';
window.SingleFileDiff = (function() { const WRAPPER = '<div class="diff-content"></div>';
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER; const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
const ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>';
const COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
WRAPPER = '<div class="diff-content"></div>'; export default class SingleFileDiff {
constructor(file) {
LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>';
COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
function SingleFileDiff(file) {
this.file = file; this.file = file;
this.toggleDiff = this.toggleDiff.bind(this); this.toggleDiff = this.toggleDiff.bind(this);
this.content = $('.diff-content', this.file); this.content = $('.diff-content', this.file);
...@@ -37,7 +32,7 @@ window.SingleFileDiff = (function() { ...@@ -37,7 +32,7 @@ window.SingleFileDiff = (function() {
}).bind(this)); }).bind(this));
} }
SingleFileDiff.prototype.toggleDiff = function($target, cb) { toggleDiff($target, cb) {
if (!$target.hasClass('js-file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return; if (!$target.hasClass('js-file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
this.isOpen = !this.isOpen; this.isOpen = !this.isOpen;
if (!this.isOpen && !this.hasError) { if (!this.isOpen && !this.hasError) {
...@@ -58,9 +53,9 @@ window.SingleFileDiff = (function() { ...@@ -58,9 +53,9 @@ window.SingleFileDiff = (function() {
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right'); this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
return this.getContentHTML(cb); return this.getContentHTML(cb);
} }
}; }
SingleFileDiff.prototype.getContentHTML = function(cb) { getContentHTML(cb) {
this.collapsedContent.hide(); this.collapsedContent.hide();
this.loadingContent.show(); this.loadingContent.show();
$.get(this.diffForPath, (function(_this) { $.get(this.diffForPath, (function(_this) {
...@@ -84,15 +79,5 @@ window.SingleFileDiff = (function() { ...@@ -84,15 +79,5 @@ window.SingleFileDiff = (function() {
if (cb) cb(); if (cb) cb();
}; };
})(this)); })(this));
}; }
}
return SingleFileDiff;
})();
$.fn.singleFileDiff = function() {
return this.each(function() {
if (!$.data(this, 'singleFileDiff')) {
return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this));
}
});
};
/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, max-len */ function SnippetsList() {
const $holder = $('.snippets-list-holder');
window.gl.SnippetsList = function() {
var $holder = $('.snippets-list-holder');
$holder.find('.pagination').on('ajax:success', (e, data) => { $holder.find('.pagination').on('ajax:success', (e, data) => {
$holder.replaceWith(data.html); $holder.replaceWith(data.html);
}); });
}; }
window.gl.SnippetsList = SnippetsList;
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */
/* global Flash */ /* global Flash */
window.Star = (function() { export default class Star {
function Star() { constructor() {
$('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) { $('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) {
var $starIcon, $starSpan, $this, toggleStar; var $starIcon, $starSpan, $this, toggleStar;
$this = $(this); $this = $(this);
...@@ -23,6 +23,4 @@ window.Star = (function() { ...@@ -23,6 +23,4 @@ window.Star = (function() {
new Flash('Star toggle failed. Try again later.', 'alert'); new Flash('Star toggle failed. Try again later.', 'alert');
}); });
} }
}
return Star;
})();
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
window.SubscriptionSelect = (function() { class SubscriptionSelect {
function SubscriptionSelect() { constructor() {
$('.js-subscription-event').each(function(i, el) { $('.js-subscription-event').each(function(i, el) {
var fieldName; var fieldName;
fieldName = $(el).data("field-name"); fieldName = $(el).data("field-name");
...@@ -28,6 +28,6 @@ window.SubscriptionSelect = (function() { ...@@ -28,6 +28,6 @@ window.SubscriptionSelect = (function() {
}); });
}); });
} }
}
return SubscriptionSelect; window.SubscriptionSelect = SubscriptionSelect;
})();
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import 'deckar01-task_list'; import 'deckar01-task_list';
class TaskList { export default class TaskList {
constructor(options = {}) { constructor(options = {}) {
this.selector = options.selector; this.selector = options.selector;
this.dataType = options.dataType; this.dataType = options.dataType;
...@@ -48,6 +48,3 @@ class TaskList { ...@@ -48,6 +48,3 @@ class TaskList {
}); });
} }
} }
window.gl = window.gl || {};
window.gl.TaskList = TaskList;
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import UsersSelect from './users_select'; import UsersSelect from './users_select';
class Todos { export default class Todos {
constructor() { constructor() {
this.initFilters(); this.initFilters();
this.bindEvents(); this.bindEvents();
...@@ -159,6 +159,3 @@ class Todos { ...@@ -159,6 +159,3 @@ class Todos {
} }
} }
} }
window.gl = window.gl || {};
gl.Todos = Todos;
/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, class-methods-use-this */
window.TreeView = (function() { export default class TreeView {
function TreeView() { constructor() {
this.initKeyNav(); this.initKeyNav();
// Code browser tree slider // Code browser tree slider
// Make the entire tree-item row clickable, but not if clicking another link (like a commit message) // Make the entire tree-item row clickable, but not if clicking another link (like a commit message)
...@@ -22,7 +22,7 @@ window.TreeView = (function() { ...@@ -22,7 +22,7 @@ window.TreeView = (function() {
$('span.log_loading:first').removeClass('hide'); $('span.log_loading:first').removeClass('hide');
} }
TreeView.prototype.initKeyNav = function() { initKeyNav() {
var li, liSelected; var li, liSelected;
li = $("tr.tree-item"); li = $("tr.tree-item");
liSelected = null; liSelected = null;
...@@ -60,7 +60,5 @@ window.TreeView = (function() { ...@@ -60,7 +60,5 @@ window.TreeView = (function() {
} }
} }
}); });
}; }
}
return TreeView;
})();
function UsagePing() { export default function UsagePing() {
const usageDataUrl = $('.usage-data').data('endpoint'); const usageDataUrl = $('.usage-data').data('endpoint');
$.ajax({ $.ajax({
...@@ -10,6 +10,3 @@ function UsagePing() { ...@@ -10,6 +10,3 @@ function UsagePing() {
}, },
}); });
} }
window.gl = window.gl || {};
window.gl.UsagePing = UsagePing;
/* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign */ /* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import UserTabs from './user_tabs';
class User { class User {
constructor({ action }) { constructor({ action }) {
...@@ -17,7 +18,7 @@ class User { ...@@ -17,7 +18,7 @@ class User {
} }
initTabs() { initTabs() {
return new window.gl.UserTabs({ return new UserTabs({
parentEl: '.user-profile', parentEl: '.user-profile',
action: this.action action: this.action
}); });
......
...@@ -60,7 +60,7 @@ content on the Users#show page. ...@@ -60,7 +60,7 @@ content on the Users#show page.
</div> </div>
*/ */
class UserTabs { export default class UserTabs {
constructor ({ defaultAction, action, parentEl }) { constructor ({ defaultAction, action, parentEl }) {
this.loaded = {}; this.loaded = {};
this.defaultAction = defaultAction || 'activity'; this.defaultAction = defaultAction || 'activity';
...@@ -171,6 +171,3 @@ class UserTabs { ...@@ -171,6 +171,3 @@ class UserTabs {
return this.$parentEl.find('.nav-links .active a').data('action'); return this.$parentEl.find('.nav-links .active a').data('action');
} }
} }
window.gl = window.gl || {};
window.gl.UserTabs = UserTabs;
...@@ -8,7 +8,7 @@ const successMessageSelector = '.username .validation-success'; ...@@ -8,7 +8,7 @@ const successMessageSelector = '.username .validation-success';
const pendingMessageSelector = '.username .validation-pending'; const pendingMessageSelector = '.username .validation-pending';
const invalidMessageSelector = '.username .gl-field-error'; const invalidMessageSelector = '.username .gl-field-error';
class UsernameValidator { export default class UsernameValidator {
constructor() { constructor() {
this.inputElement = $('#new_user_username'); this.inputElement = $('#new_user_username');
this.inputDomElement = this.inputElement.get(0); this.inputDomElement = this.inputElement.get(0);
...@@ -129,5 +129,3 @@ class UsernameValidator { ...@@ -129,5 +129,3 @@ class UsernameValidator {
$inputErrorMessage.show(); $inputErrorMessage.show();
} }
} }
window.UsernameValidator = UsernameValidator;
...@@ -3,6 +3,3 @@ export default class VersionCheckImage { ...@@ -3,6 +3,3 @@ export default class VersionCheckImage {
imageElement.off('error').on('error', () => imageElement.hide()); imageElement.off('error').on('error', () => imageElement.hide());
} }
} }
window.gl = window.gl || {};
gl.VersionCheckImage = VersionCheckImage;
class VisibilitySelect { export default class VisibilitySelect {
constructor(container) { constructor(container) {
if (!container) throw new Error('VisibilitySelect requires a container element as argument 1'); if (!container) throw new Error('VisibilitySelect requires a container element as argument 1');
this.container = container; this.container = container;
...@@ -19,6 +19,3 @@ class VisibilitySelect { ...@@ -19,6 +19,3 @@ class VisibilitySelect {
this.helpBlock.textContent = this.select.querySelector('option:checked').dataset.description; this.helpBlock.textContent = this.select.querySelector('option:checked').dataset.description;
} }
} }
window.gl = window.gl || {};
window.gl.VisibilitySelect = VisibilitySelect;
/* eslint-disable no-param-reassign */
/* global Breakpoints */ /* global Breakpoints */
import 'vendor/jquery.nicescroll'; import 'vendor/jquery.nicescroll';
import './breakpoints'; import './breakpoints';
class Wikis { export default class Wikis {
constructor() { constructor() {
this.bp = Breakpoints.get(); this.bp = Breakpoints.get();
this.sidebarEl = document.querySelector('.js-wiki-sidebar'); this.sidebarEl = document.querySelector('.js-wiki-sidebar');
...@@ -63,6 +62,3 @@ class Wikis { ...@@ -63,6 +62,3 @@ class Wikis {
} }
} }
} }
window.gl = window.gl || {};
window.gl.Wikis = Wikis;
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, max-len, class-methods-use-this */
/* global Mousetrap */ /* global Mousetrap */
// Zen Mode (full screen) textarea // Zen Mode (full screen) textarea
...@@ -35,8 +35,8 @@ window.Dropzone = Dropzone; ...@@ -35,8 +35,8 @@ window.Dropzone = Dropzone;
// **Target** a.js-zen-leave // **Target** a.js-zen-leave
// //
window.ZenMode = (function() { export default class ZenMode {
function ZenMode() { constructor() {
this.active_backdrop = null; this.active_backdrop = null;
this.active_textarea = null; this.active_textarea = null;
$(document).on('click', '.js-zen-enter', function(e) { $(document).on('click', '.js-zen-enter', function(e) {
...@@ -66,7 +66,7 @@ window.ZenMode = (function() { ...@@ -66,7 +66,7 @@ window.ZenMode = (function() {
}); });
} }
ZenMode.prototype.enter = function(backdrop) { enter(backdrop) {
Mousetrap.pause(); Mousetrap.pause();
this.active_backdrop = $(backdrop); this.active_backdrop = $(backdrop);
this.active_backdrop.addClass('fullscreen'); this.active_backdrop.addClass('fullscreen');
...@@ -74,9 +74,9 @@ window.ZenMode = (function() { ...@@ -74,9 +74,9 @@ window.ZenMode = (function() {
// Prevent a user-resized textarea from persisting to fullscreen // Prevent a user-resized textarea from persisting to fullscreen
this.active_textarea.removeAttr('style'); this.active_textarea.removeAttr('style');
return this.active_textarea.focus(); return this.active_textarea.focus();
}; }
ZenMode.prototype.exit = function() { exit() {
if (this.active_textarea) { if (this.active_textarea) {
Mousetrap.unpause(); Mousetrap.unpause();
this.active_textarea.closest('.zen-backdrop').removeClass('fullscreen'); this.active_textarea.closest('.zen-backdrop').removeClass('fullscreen');
...@@ -85,13 +85,11 @@ window.ZenMode = (function() { ...@@ -85,13 +85,11 @@ window.ZenMode = (function() {
this.active_backdrop = null; this.active_backdrop = null;
return Dropzone.forElement('.div-dropzone').enable(); return Dropzone.forElement('.div-dropzone').enable();
} }
}; }
ZenMode.prototype.scrollTo = function(zen_area) { scrollTo(zen_area) {
return $.scrollTo(zen_area, 0, { return $.scrollTo(zen_area, 0, {
offset: -150 offset: -150
}); });
}; }
}
return ZenMode;
})();
.blank-state-parent-container {
display: flex;
.section-container {
display: flex;
flex: 1;
padding: 10px;
}
.section-body {
width: 100%;
height: 100%;
padding-bottom: 25px;
border: 1px solid $border-color;
border-radius: $border-radius-default;
&.section-ee-trial {
display: flex;
align-items: center;
justify-content: center;
}
}
}
.blank-state-welcome { .blank-state-welcome {
text-align: center; text-align: center;
border-bottom: 1px solid $border-color;
.blank-state-text { .blank-state-text {
margin-bottom: 0; margin-bottom: 0;
...@@ -10,6 +33,10 @@ ...@@ -10,6 +33,10 @@
.blank-state { .blank-state {
padding-top: 20px; padding-top: 20px;
padding-bottom: 20px; padding-bottom: 20px;
}
.blank-state.ee-trial {
padding: 20px;
text-align: center; text-align: center;
} }
...@@ -20,20 +47,24 @@ ...@@ -20,20 +47,24 @@
.blank-state-icon { .blank-state-icon {
padding-bottom: 20px; padding-bottom: 20px;
color: $gray-darkest;
font-size: 56px; font-size: 56px;
path, svg {
polygon { display: block;
fill: currentColor; margin: auto;
}
}
@media (min-width: $screen-sm-max) {
.section-welcome .blank-state-icon svg {
width: 130%;
} }
} }
.blank-state-title { .blank-state-title {
margin-top: 0; margin-top: 0;
margin-bottom: 5px; margin-bottom: 10px;
font-size: 18px; font-size: 18px;
font-weight: normal;
} }
.blank-state-text { .blank-state-text {
...@@ -49,3 +80,24 @@ ...@@ -49,3 +80,24 @@
.blank-state-welcome-title { .blank-state-welcome-title {
font-size: 24px; font-size: 24px;
} }
@media (max-width: $screen-md-min) {
.blank-state-parent-container {
&,
.section-container {
display: block;
}
}
.blank-state {
text-align: center;
}
.blank-state-icon {
padding-bottom: 0;
}
.blank-state-body {
margin-top: 15px;
}
}
...@@ -20,17 +20,29 @@ ...@@ -20,17 +20,29 @@
color: $text; color: $text;
border-color: $border; border-color: $border;
> .icon {
color: $text;
}
&:hover, &:hover,
&:focus { &:focus {
background-color: $hover-background; background-color: $hover-background;
border-color: $hover-border; border-color: $hover-border;
color: $hover-text; color: $hover-text;
> .icon {
color: $hover-text;
}
} }
&:active { &:active {
background-color: $active-background; background-color: $active-background;
border-color: $active-border; border-color: $active-border;
color: $hover-text; color: $hover-text;
> .icon {
color: $hover-text;
}
} }
} }
...@@ -163,7 +175,8 @@ ...@@ -163,7 +175,8 @@
@include btn-orange; @include btn-orange;
} }
&.btn-close { &.btn-close,
&.btn-close-color {
@include btn-outline($white-light, $orange-600, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700); @include btn-outline($white-light, $orange-600, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700);
} }
...@@ -181,7 +194,8 @@ ...@@ -181,7 +194,8 @@
float: right; float: right;
} }
&.btn-reopen { &.btn-reopen,
.btn-reopen-color {
/* should be same as parent class for now */ /* should be same as parent class for now */
} }
......
...@@ -295,9 +295,74 @@ ...@@ -295,9 +295,74 @@
} }
} }
.filtered-search-box-input-container .dropdown-menu, .droplab-dropdown {
.filtered-search-box-input-container .dropdown-menu-nav, .description {
.comment-type-dropdown .dropdown-menu { display: inline-block;
white-space: normal;
margin-left: 5px;
}
.dropdown-toggle > i {
pointer-events: none;
}
li {
padding: $gl-btn-padding $gl-btn-padding 2px;
cursor: pointer;
> a,
> button {
display: flex;
margin: 0;
padding: 0;
border-radius: 0;
text-overflow: inherit;
background-color: inherit;
color: inherit;
border: inherit;
text-align: left;
&:hover,
&:focus {
background-color: inherit;
color: inherit;
}
&.btn .fa:not(:last-child) {
margin-left: 5px;
}
}
&:hover,
&:focus {
background-color: $dropdown-hover-color;
color: $white-light;
}
&.droplab-item-selected i {
visibility: visible;
}
.icon {
visibility: hidden;
}
}
.icon {
display: inline-block;
vertical-align: top;
padding-top: 2px;
}
.divider {
margin: 0 8px;
padding: 0;
border-top: $gray-darkest;
}
}
.droplab-dropdown .dropdown-menu,
.droplab-dropdown .dropdown-menu-nav {
display: none; display: none;
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
......
...@@ -70,6 +70,13 @@ ...@@ -70,6 +70,13 @@
.input-token { .input-token {
max-width: 200px; max-width: 200px;
padding: 0;
&:hover,
&:focus {
background-color: inherit;
color: inherit;
}
} }
.input-token:only-child, .input-token:only-child,
...@@ -156,6 +163,16 @@ ...@@ -156,6 +163,16 @@
} }
} }
.droplab-dropdown li.filtered-search-token {
padding: 0;
&:hover,
&:focus {
background-color: inherit;
color: inherit;
}
}
.filtered-search-term { .filtered-search-term {
.name { .name {
background-color: inherit; background-color: inherit;
......
...@@ -349,6 +349,12 @@ ul.indent-list { ...@@ -349,6 +349,12 @@ ul.indent-list {
.group-row { .group-row {
padding: 0; padding: 0;
border: none; border: none;
&:last-of-type {
.group-row-contents:not(:hover) {
border-bottom: 1px solid transparent;
}
}
} }
.group-row-contents { .group-row-contents {
......
...@@ -276,7 +276,7 @@ $diff-view-modes-border: #c1c1c1; ...@@ -276,7 +276,7 @@ $diff-view-modes-border: #c1c1c1;
/* /*
* Fonts * Fonts
*/ */
$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; $monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
/* /*
......
...@@ -54,6 +54,12 @@ $new-sidebar-width: 220px; ...@@ -54,6 +54,12 @@ $new-sidebar-width: 220px;
} }
} }
} }
.project-title,
.group-title {
overflow: hidden;
text-overflow: ellipsis;
}
} }
.settings-avatar { .settings-avatar {
......
...@@ -800,6 +800,31 @@ ...@@ -800,6 +800,31 @@
} }
} }
.issuable-close-button,
.issuable-close-toggle {
@include transition(border-color, color);
}
.issuable-close-dropdown {
.dropdown-menu {
min-width: 270px;
left: auto;
right: 0;
}
.description {
margin-bottom: 10px;
.text {
margin: 0;
}
}
.dropdown-toggle > .icon {
margin: 0 3px;
}
}
.add-issuable-form-input-wrapper { .add-issuable-form-input-wrapper {
height: auto; height: auto;
padding: $gl-vert-padding $gl-vert-padding 0 $gl-input-padding; padding: $gl-vert-padding $gl-vert-padding 0 $gl-input-padding;
......
...@@ -356,7 +356,6 @@ ...@@ -356,7 +356,6 @@
color: $white-light; color: $white-light;
padding-right: 2px; padding-right: 2px;
margin-top: 2px; margin-top: 2px;
pointer-events: none;
} }
} }
...@@ -366,56 +365,6 @@ ...@@ -366,56 +365,6 @@
width: 298px; width: 298px;
} }
.description {
display: inline-block;
white-space: normal;
margin-left: 8px;
padding-right: 33px;
}
li {
padding-top: 6px;
& > a {
margin: 0;
padding: 0;
color: inherit;
border-radius: 0;
text-overflow: inherit;
&:hover,
&:focus {
background-color: inherit;
color: inherit;
}
}
&:hover,
&:focus {
background-color: $dropdown-hover-color;
color: $white-light;
}
&.droplab-item-selected i {
visibility: visible;
}
i {
visibility: hidden;
}
}
i {
display: inline-block;
vertical-align: top;
padding-top: 2px;
}
.divider {
margin: 0 8px;
padding: 0;
border-top: $gray-darkest;
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
display: flex; display: flex;
......
...@@ -286,8 +286,7 @@ table.u2f-registrations { ...@@ -286,8 +286,7 @@ table.u2f-registrations {
} }
.user-callout { .user-callout {
margin: 0 auto; margin: 20px -5px 0;
max-width: $screen-lg-min;
.bordered-box { .bordered-box {
border: 1px solid $blue-300; border: 1px solid $blue-300;
......
...@@ -79,7 +79,7 @@ module MembershipActions ...@@ -79,7 +79,7 @@ module MembershipActions
def members_page_url def members_page_url
if membershipable.is_a?(Project) if membershipable.is_a?(Project)
project_settings_members_path(membershipable) project_project_members_path(membershipable)
else else
polymorphic_url([membershipable, :members]) polymorphic_url([membershipable, :members])
end end
......
...@@ -2,13 +2,13 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -2,13 +2,13 @@ class Groups::MilestonesController < Groups::ApplicationController
include MilestoneActions include MilestoneActions
before_action :group_projects before_action :group_projects
before_action :milestone, only: [:show, :update, :merge_requests, :participants, :labels] before_action :milestone, only: [:edit, :show, :update, :merge_requests, :participants, :labels]
before_action :authorize_admin_milestones!, only: [:new, :create, :update] before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update]
def index def index
respond_to do |format| respond_to do |format|
format.html do format.html do
@milestone_states = GlobalMilestone.states_count(@projects) @milestone_states = GlobalMilestone.states_count(group_projects, group)
@milestones = Kaminari.paginate_array(milestones).page(params[:page]) @milestones = Kaminari.paginate_array(milestones).page(params[:page])
end end
format.json do format.json do
...@@ -22,49 +22,41 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -22,49 +22,41 @@ class Groups::MilestonesController < Groups::ApplicationController
end end
def create def create
project_ids = params[:milestone][:project_ids].reject(&:blank?) @milestone = Milestones::CreateService.new(group, current_user, milestone_params).execute
title = milestone_params[:title]
if create_milestones(project_ids) if @milestone.persisted?
redirect_to milestone_path(title) redirect_to milestone_path
else else
render_new_with_error(project_ids.empty?) render "new"
end end
end end
def show def show
end end
def update def edit
@milestone.milestones.each do |milestone| render_404 if @milestone.is_legacy_group_milestone?
Milestones::UpdateService.new(milestone.project, current_user, milestone_params).execute(milestone)
end
redirect_back_or_default(default: milestone_path(@milestone.title))
end end
private def update
# Keep this compatible with legacy group milestones where we have to update
def create_milestones(project_ids) # all projects milestones states at once.
return false unless project_ids.present? if @milestone.is_legacy_group_milestone?
update_params = milestone_params.select { |key| key == "state_event" }
milestones = @milestone.milestones
else
update_params = milestone_params
milestones = [@milestone]
end
ActiveRecord::Base.transaction do milestones.each do |milestone|
@projects.where(id: project_ids).each do |project| Milestones::UpdateService.new(milestone.parent, current_user, update_params).execute(milestone)
Milestones::CreateService.new(project, current_user, milestone_params).execute
end
end end
true redirect_to milestone_path
rescue ActiveRecord::ActiveRecordError => e
flash.now[:alert] = "An error occurred while creating the milestone: #{e.message}"
false
end end
def render_new_with_error(empty_project_ids) private
@milestone = Milestone.new(milestone_params)
@milestone.errors.add(:base, "Please select at least one project.") if empty_project_ids
render :new
end
def authorize_admin_milestones! def authorize_admin_milestones!
return render_404 unless can?(current_user, :admin_milestones, group) return render_404 unless can?(current_user, :admin_milestones, group)
...@@ -74,16 +66,31 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -74,16 +66,31 @@ class Groups::MilestonesController < Groups::ApplicationController
params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event) params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
end end
def milestone_path(title) def milestone_path
group_milestone_path(@group, title.to_slug.to_s, title: title) if @milestone.is_legacy_group_milestone?
group_milestone_path(group, @milestone.safe_title, title: @milestone.title)
else
group_milestone_path(group, @milestone.iid)
end
end end
def milestones def milestones
@milestones = GroupMilestone.build_collection(@group, @projects, params) search_params = params.merge(group_ids: group.id)
milestones = MilestonesFinder.new(search_params).execute
legacy_milestones = GroupMilestone.build_collection(group, group_projects, params)
milestones + legacy_milestones
end end
def milestone def milestone
@milestone = GroupMilestone.build(@group, @projects, params[:title]) @milestone =
if params[:title]
GroupMilestone.build(group, group_projects, params[:title])
else
group.milestones.find_by_iid(params[:id])
end
render_404 unless @milestone render_404 unless @milestone
end end
end end
...@@ -22,7 +22,7 @@ class Projects::GroupLinksController < Projects::ApplicationController ...@@ -22,7 +22,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
flash[:alert] = 'Please select a group.' flash[:alert] = 'Please select a group.'
end end
redirect_to project_settings_members_path(project) redirect_to project_project_members_path(project)
end end
def update def update
...@@ -36,7 +36,7 @@ class Projects::GroupLinksController < Projects::ApplicationController ...@@ -36,7 +36,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
redirect_to project_settings_members_path(project), status: 302 redirect_to project_project_members_path(project), status: 302
end end
format.js { head :ok } format.js { head :ok }
end end
......
...@@ -19,8 +19,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont ...@@ -19,8 +19,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
end end
def merge_request_params def merge_request_params
params.require(:merge_request) params.require(:merge_request).permit(merge_request_params_attributes)
.permit(merge_request_params_attributes)
end end
def merge_request_params_attributes def merge_request_params_attributes
......
...@@ -13,20 +13,16 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -13,20 +13,16 @@ class Projects::MilestonesController < Projects::ApplicationController
respond_to :html respond_to :html
def index def index
@milestones =
case params[:state]
when 'all' then @project.milestones
when 'closed' then @project.milestones.closed
else @project.milestones.active
end
@sort = params[:sort] || 'due_date_asc' @sort = params[:sort] || 'due_date_asc'
@milestones = @milestones.sort(@sort) @milestones = milestones.sort(@sort)
respond_to do |format| respond_to do |format|
format.html do format.html do
@project_namespace = @project.namespace.becomes(Namespace) @project_namespace = @project.namespace.becomes(Namespace)
@milestones = @milestones.includes(:project) # We need to show group milestones in the JSON response
# so that people can filter by and assign group milestones,
# but we don't need to show them on the project milestones page itself.
@milestones = @milestones.for_projects
@milestones = @milestones.page(params[:page]) @milestones = @milestones.page(params[:page])
end end
format.json do format.json do
...@@ -45,6 +41,8 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -45,6 +41,8 @@ class Projects::MilestonesController < Projects::ApplicationController
end end
def show def show
@project_namespace = @project.namespace.becomes(Namespace)
if @project.feature_available?(:burndown_charts, current_user) && if @project.feature_available?(:burndown_charts, current_user) &&
@project.feature_available?(:issue_weights, current_user) @project.feature_available?(:issue_weights, current_user)
@burndown = Burndown.new(@milestone) @burndown = Burndown.new(@milestone)
...@@ -54,7 +52,7 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -54,7 +52,7 @@ class Projects::MilestonesController < Projects::ApplicationController
def create def create
@milestone = Milestones::CreateService.new(project, current_user, milestone_params).execute @milestone = Milestones::CreateService.new(project, current_user, milestone_params).execute
if @milestone.save if @milestone.valid?
redirect_to project_milestone_path(@project, @milestone) redirect_to project_milestone_path(@project, @milestone)
else else
render "new" render "new"
...@@ -89,6 +87,18 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -89,6 +87,18 @@ class Projects::MilestonesController < Projects::ApplicationController
protected protected
def milestones
@milestones ||= begin
if @project.group && can?(current_user, :read_group, @project.group)
group = @project.group
end
search_params = params.merge(project_ids: @project.id, group_ids: group&.id)
MilestonesFinder.new(search_params).execute
end
end
def milestone def milestone
@milestone ||= @project.milestones.find_by!(iid: params[:id]) @milestone ||= @project.milestones.find_by!(iid: params[:id])
end end
......
...@@ -6,8 +6,23 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -6,8 +6,23 @@ class Projects::ProjectMembersController < Projects::ApplicationController
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access] before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index def index
sort = params[:sort].presence || sort_value_name @sort = params[:sort].presence || sort_value_name
redirect_to project_settings_members_path(@project, sort: sort) @group_links = @project.project_group_links
@skip_groups = @group_links.pluck(:group_id)
@skip_groups << @project.namespace_id unless @project.personal?
@skip_groups += @project.group.ancestors.pluck(:id) if @project.group
@project_members = MembersFinder.new(@project, current_user).execute
if params[:search].present?
@project_members = @project_members.joins(:user).merge(User.search(params[:search]))
@group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end
@project_members = @project_members.sort(@sort).page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
@project_member = @project.project_members.new
end end
def update def update
...@@ -23,7 +38,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -23,7 +38,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end end
def resend_invite def resend_invite
redirect_path = project_settings_members_path(@project) redirect_path = project_project_members_path(@project)
@project_member = @project.project_members.find(params[:id]) @project_member = @project.project_members.find(params[:id])
...@@ -46,7 +61,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -46,7 +61,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
return render_404 return render_404
end end
redirect_to(project_settings_members_path(project), redirect_to(project_project_members_path(project),
notice: notice) notice: notice)
end end
......
module Projects
module Settings
class MembersController < Projects::ApplicationController
include SortingHelper
def show
@sort = params[:sort].presence || sort_value_name
@group_links = @project.project_group_links
@skip_groups = @group_links.pluck(:group_id)
@skip_groups << @project.namespace_id unless @project.personal?
@skip_groups += @project.group.ancestors.pluck(:id) if @project.group
@project_members = MembersFinder.new(@project, current_user).execute
if params[:search].present?
@project_members = @project_members.joins(:user).merge(User.search(params[:search]))
@group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end
@project_members = @project_members.sort(@sort).page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
@project_member = @project.project_members.new
end
end
end
end
...@@ -154,9 +154,17 @@ class IssuableFinder ...@@ -154,9 +154,17 @@ class IssuableFinder
@milestones = @milestones =
if milestones? if milestones?
scope = Milestone.where(project_id: projects) if project?
group_id = project.group&.id
project_id = project.id
end
group_id = group.id if group
scope.where(title: params[:milestone_title]) search_params =
{ title: params[:milestone_title], project_ids: project_id, group_ids: group_id }
MilestonesFinder.new(search_params).execute
else else
Milestone.none Milestone.none
end end
...@@ -338,11 +346,6 @@ class IssuableFinder ...@@ -338,11 +346,6 @@ class IssuableFinder
items = items.left_joins_milestones.where('milestones.start_date <= NOW()') items = items.left_joins_milestones.where('milestones.start_date <= NOW()')
else else
items = items.with_milestone(params[:milestone_title]) items = items.with_milestone(params[:milestone_title])
items_projects = projects(items)
if items_projects
items = items.where(milestones: { project_id: items_projects })
end
end end
end end
......
# Search for milestones
#
# params - Hash
# project_ids: Array of project ids or single project id.
# group_ids: Array of group ids or single group id.
# order - Orders by field default due date asc.
# title - filter by title.
# state - filters by state.
class MilestonesFinder class MilestonesFinder
def execute(projects, params) attr_reader :params, :project_ids, :group_ids
milestones = Milestone.of_projects(projects)
milestones = milestones.reorder("due_date ASC") def initialize(params = {})
@project_ids = Array(params[:project_ids])
case params[:state] @group_ids = Array(params[:group_ids])
when 'closed' then milestones.closed @params = params
when 'all' then milestones end
else milestones.active
def execute
return Milestone.none if project_ids.empty? && group_ids.empty?
items = Milestone.all
items = by_groups_and_projects(items)
items = by_title(items)
items = by_state(items)
order(items)
end
private
def by_groups_and_projects(items)
items.for_projects_and_groups(project_ids, group_ids)
end
def by_title(items)
if params[:title]
items.where(title: params[:title])
else
items
end
end
def by_state(items)
Milestone.filter_by_state(items, params[:state])
end
def order(items)
if params.has_key?(:order)
items.reorder(params[:order])
else
order_statement = Gitlab::Database.nulls_last_order('due_date', 'ASC')
items.reorder(order_statement)
end end
end end
end end
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
module GitlabRoutingHelper module GitlabRoutingHelper
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do
Gitlab::Routing.includes_helpers(self)
end
# Project # Project
def project_tree_path(project, ref = nil, *args) def project_tree_path(project, ref = nil, *args)
namespace_project_tree_path(project.namespace, project, ref || @ref || project.repository.root_ref, *args) # rubocop:disable Cop/ProjectPathHelper namespace_project_tree_path(project.namespace, project, ref || @ref || project.repository.root_ref, *args) # rubocop:disable Cop/ProjectPathHelper
...@@ -97,7 +101,7 @@ module GitlabRoutingHelper ...@@ -97,7 +101,7 @@ module GitlabRoutingHelper
## Members ## Members
def project_members_url(project, *args) def project_members_url(project, *args)
project_project_members_url(project) project_project_members_url(project, *args)
end end
def project_member_path(project_member, *args) def project_member_path(project_member, *args)
......
...@@ -253,6 +253,53 @@ module IssuablesHelper ...@@ -253,6 +253,53 @@ module IssuablesHelper
@counts[cache_key][state] @counts[cache_key][state]
end end
def close_issuable_url(issuable)
issuable_url(issuable, close_reopen_params(issuable, :close))
end
def reopen_issuable_url(issuable)
issuable_url(issuable, close_reopen_params(issuable, :reopen))
end
def close_reopen_issuable_url(issuable, should_inverse = false)
issuable.closed? ^ should_inverse ? reopen_issuable_url(issuable) : close_issuable_url(issuable)
end
def issuable_url(issuable, *options)
case issuable
when Issue
issue_url(issuable, *options)
when MergeRequest
merge_request_url(issuable, *options)
end
end
def issuable_button_visibility(issuable, closed)
case issuable
when Issue
issue_button_visibility(issuable, closed)
when MergeRequest
merge_request_button_visibility(issuable, closed)
end
end
def issuable_close_reopen_button_method(issuable)
case issuable
when Issue
''
when MergeRequest
'put'
end
end
def issuable_author_is_current_user(issuable)
issuable.author == current_user
end
def issuable_display_type(issuable)
issuable.model_name.human.downcase
end
private private
def sidebar_gutter_collapsed? def sidebar_gutter_collapsed?
...@@ -278,8 +325,6 @@ module IssuablesHelper ...@@ -278,8 +325,6 @@ module IssuablesHelper
issue_template_names issue_template_names
when MergeRequest when MergeRequest
merge_request_template_names merge_request_template_names
else
raise 'Unknown issuable type!'
end end
end end
...@@ -309,4 +354,12 @@ module IssuablesHelper ...@@ -309,4 +354,12 @@ module IssuablesHelper
container: (is_collapsed ? 'body' : nil) container: (is_collapsed ? 'body' : nil)
} }
end end
def close_reopen_params(issuable, action)
{
issuable.model_name.to_s.underscore => { state_event: action }
}.tap do |params|
params[:format] = :json if issuable.is_a?(Issue)
end
end
end end
...@@ -54,8 +54,10 @@ module MilestonesHelper ...@@ -54,8 +54,10 @@ module MilestonesHelper
def milestone_class_for_state(param, check, match_blank_param = false) def milestone_class_for_state(param, check, match_blank_param = false)
if match_blank_param if match_blank_param
'active' if param.blank? || param == check 'active' if param.blank? || param == check
elsif param == check
'active'
else else
'active' if param == check check
end end
end end
...@@ -179,4 +181,14 @@ module MilestonesHelper ...@@ -179,4 +181,14 @@ module MilestonesHelper
labels_dashboard_milestone_path(milestone, title: milestone.title, format: :json) labels_dashboard_milestone_path(milestone, title: milestone.title, format: :json)
end end
end end
def group_milestone_route(milestone, params = {})
params = nil if params.empty?
if milestone.is_legacy_group_milestone?
group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: params)
else
group_milestone_path(@group, milestone.iid, milestone: params)
end
end
end end
...@@ -267,15 +267,15 @@ module ProjectsHelper ...@@ -267,15 +267,15 @@ module ProjectsHelper
def tab_ability_map def tab_ability_map
{ {
environments: :read_environment, environments: :read_environment,
milestones: :read_milestone, milestones: :read_milestone,
snippets: :read_project_snippet, snippets: :read_project_snippet,
settings: :admin_project, settings: :admin_project,
builds: :read_build, builds: :read_build,
labels: :read_label, labels: :read_label,
issues: :read_issue, issues: :read_issue,
team: :read_project_member, project_members: :read_project_member,
wiki: :read_wiki wiki: :read_wiki
} }
end end
......
...@@ -85,7 +85,7 @@ module SearchHelper ...@@ -85,7 +85,7 @@ module SearchHelper
{ category: "Current Project", label: "Merge Requests", url: project_merge_requests_path(@project) }, { category: "Current Project", label: "Merge Requests", url: project_merge_requests_path(@project) },
{ category: "Current Project", label: "Milestones", url: project_milestones_path(@project) }, { category: "Current Project", label: "Milestones", url: project_milestones_path(@project) },
{ category: "Current Project", label: "Snippets", url: project_snippets_path(@project) }, { category: "Current Project", label: "Snippets", url: project_snippets_path(@project) },
{ category: "Current Project", label: "Members", url: project_settings_members_path(@project) }, { category: "Current Project", label: "Members", url: project_project_members_path(@project) },
{ category: "Current Project", label: "Wiki", url: project_wikis_path(@project) } { category: "Current Project", label: "Wiki", url: project_wikis_path(@project) }
] ]
else else
......
...@@ -2,6 +2,7 @@ module Ci ...@@ -2,6 +2,7 @@ module Ci
class Variable < ActiveRecord::Base class Variable < ActiveRecord::Base
extend Ci::Model extend Ci::Model
include HasVariable include HasVariable
include Presentable
prepend EE::Ci::Variable prepend EE::Ci::Variable
include Presentable include Presentable
......
...@@ -8,7 +8,8 @@ module InternalId ...@@ -8,7 +8,8 @@ module InternalId
def set_iid def set_iid
if iid.blank? if iid.blank?
records = project.send(self.class.name.tableize) parent = project || group
records = parent.send(self.class.name.tableize)
records = records.with_deleted if self.paranoid? records = records.with_deleted if self.paranoid?
max_iid = records.maximum(:iid) max_iid = records.maximum(:iid)
......
...@@ -30,6 +30,7 @@ module Issuable ...@@ -30,6 +30,7 @@ module Issuable
belongs_to :updated_by, class_name: "User" belongs_to :updated_by, class_name: "User"
belongs_to :last_edited_by, class_name: 'User' belongs_to :last_edited_by, class_name: 'User'
belongs_to :milestone belongs_to :milestone
has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do # rubocop:disable Cop/ActiveRecordDependent has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do # rubocop:disable Cop/ActiveRecordDependent
def authors_loaded? def authors_loaded?
# We check first if we're loaded to not load unnecessarily. # We check first if we're loaded to not load unnecessarily.
......
...@@ -70,6 +70,22 @@ module Milestoneish ...@@ -70,6 +70,22 @@ module Milestoneish
due_date && due_date.past? due_date && due_date.past?
end end
def is_group_milestone?
false
end
def is_project_milestone?
false
end
def is_legacy_group_milestone?
false
end
def is_dashboard_milestone?
false
end
private private
def count_issues_by_state(user) def count_issues_by_state(user)
......
...@@ -3,6 +3,8 @@ module ShaAttribute ...@@ -3,6 +3,8 @@ module ShaAttribute
module ClassMethods module ClassMethods
def sha_attribute(name) def sha_attribute(name)
return unless table_exists?
column = columns.find { |c| c.name == name.to_s } column = columns.find { |c| c.name == name.to_s }
# In case the table doesn't exist we won't be able to find the column, # In case the table doesn't exist we won't be able to find the column,
......
...@@ -2,4 +2,8 @@ class DashboardMilestone < GlobalMilestone ...@@ -2,4 +2,8 @@ class DashboardMilestone < GlobalMilestone
def issues_finder_params def issues_finder_params
{ authorized_only: true } { authorized_only: true }
end end
def is_dashboard_milestone?
true
end
end end
...@@ -4,6 +4,7 @@ class GlobalMilestone ...@@ -4,6 +4,7 @@ class GlobalMilestone
include ::EE::GlobalMilestone include ::EE::GlobalMilestone
EPOCH = DateTime.parse('1970-01-01') EPOCH = DateTime.parse('1970-01-01')
STATE_COUNT_HASH = { opened: 0, closed: 0, all: 0 }.freeze
attr_accessor :title, :milestones attr_accessor :title, :milestones
alias_attribute :name, :title alias_attribute :name, :title
...@@ -13,7 +14,10 @@ class GlobalMilestone ...@@ -13,7 +14,10 @@ class GlobalMilestone
end end
def self.build_collection(projects, params) def self.build_collection(projects, params)
child_milestones = MilestonesFinder.new.execute(projects, params) params =
{ project_ids: projects.map(&:id), state: params[:state] }
child_milestones = MilestonesFinder.new(params).execute
milestones = child_milestones.select(:id, :title).group_by(&:title).map do |title, grouped| milestones = child_milestones.select(:id, :title).group_by(&:title).map do |title, grouped|
milestones_relation = Milestone.where(id: grouped.map(&:id)) milestones_relation = Milestone.where(id: grouped.map(&:id))
...@@ -30,13 +34,42 @@ class GlobalMilestone ...@@ -30,13 +34,42 @@ class GlobalMilestone
new(title, child_milestones) new(title, child_milestones)
end end
def self.states_count(projects) def self.states_count(projects, group = nil)
relation = MilestonesFinder.new.execute(projects, state: 'all') legacy_group_milestones_count = legacy_group_milestone_states_count(projects)
milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count group_milestones_count = group_milestones_states_count(group)
legacy_group_milestones_count.merge(group_milestones_count) do |k, legacy_group_milestones_count, group_milestones_count|
legacy_group_milestones_count + group_milestones_count
end
end
def self.group_milestones_states_count(group)
return STATE_COUNT_HASH unless group
params = { group_ids: [group.id], state: 'all', order: nil }
relation = MilestonesFinder.new(params).execute
grouped_by_state = relation.group(:state).count
{
opened: grouped_by_state['active'] || 0,
closed: grouped_by_state['closed'] || 0,
all: grouped_by_state.values.sum
}
end
# Counts the legacy group milestones which must be grouped by title
def self.legacy_group_milestone_states_count(projects)
return STATE_COUNT_HASH unless projects
params = { project_ids: projects.map(&:id), state: 'all', order: nil }
relation = MilestonesFinder.new(params).execute
project_milestones_by_state_and_title = relation.group(:state, :title).count
opened = count_by_state(milestones_by_state_and_title, 'active') opened = count_by_state(project_milestones_by_state_and_title, 'active')
closed = count_by_state(milestones_by_state_and_title, 'closed') closed = count_by_state(project_milestones_by_state_and_title, 'closed')
all = milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count all = project_milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count
{ {
opened: opened, opened: opened,
......
...@@ -22,6 +22,7 @@ class Group < Namespace ...@@ -22,6 +22,7 @@ class Group < Namespace
has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
has_many :milestones
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :shared_projects, through: :project_group_links, source: :project has_many :shared_projects, through: :project_group_links, source: :project
has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
......
...@@ -18,4 +18,8 @@ class GroupMilestone < GlobalMilestone ...@@ -18,4 +18,8 @@ class GroupMilestone < GlobalMilestone
def issues_finder_params def issues_finder_params
{ group_id: group.id } { group_id: group.id }
end end
def is_legacy_group_milestone?
true
end
end end
...@@ -58,6 +58,7 @@ class Issue < ActiveRecord::Base ...@@ -58,6 +58,7 @@ class Issue < ActiveRecord::Base
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') } scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') } scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
scope :order_weight_desc, -> { reorder('weight IS NOT NULL, weight DESC') } scope :order_weight_desc, -> { reorder('weight IS NOT NULL, weight DESC') }
scope :order_weight_asc, -> { reorder('weight ASC') } scope :order_weight_asc, -> { reorder('weight ASC') }
......
...@@ -18,7 +18,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -18,7 +18,7 @@ class MergeRequest < ActiveRecord::Base
has_many :merge_request_diffs has_many :merge_request_diffs
has_one :merge_request_diff, has_one :merge_request_diff,
-> { order('merge_request_diffs.id DESC') } -> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request
belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline" belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline"
...@@ -874,7 +874,10 @@ class MergeRequest < ActiveRecord::Base ...@@ -874,7 +874,10 @@ class MergeRequest < ActiveRecord::Base
# #
def all_commit_shas def all_commit_shas
if persisted? if persisted?
merge_request_diffs.preload(:merge_request_diff_commits).flat_map(&:commit_shas).uniq column_shas = MergeRequestDiffCommit.where(merge_request_diff: merge_request_diffs).pluck('DISTINCT(sha)')
serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas)
(column_shas + serialised_shas).uniq
elsif compare_commits elsif compare_commits
compare_commits.to_a.reverse.map(&:id) compare_commits.to_a.reverse.map(&:id)
else else
......
...@@ -21,18 +21,33 @@ class Milestone < ActiveRecord::Base ...@@ -21,18 +21,33 @@ class Milestone < ActiveRecord::Base
cache_markdown_field :description cache_markdown_field :description
belongs_to :project belongs_to :project
belongs_to :group
has_many :boards has_many :boards
has_many :issues has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests has_many :merge_requests
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_groups, ->(ids) { where(group_id: ids) }
scope :active, -> { with_state(:active) } scope :active, -> { with_state(:active) }
scope :closed, -> { with_state(:closed) } scope :closed, -> { with_state(:closed) }
scope :of_projects, ->(ids) { where(project_id: ids) } scope :for_projects, -> { where(group: nil).includes(:project) }
scope :for_projects_and_groups, -> (project_ids, group_ids) do
conditions = []
conditions << arel_table[:project_id].in(project_ids) if project_ids.compact.any?
conditions << arel_table[:group_id].in(group_ids) if group_ids.compact.any?
where(conditions.reduce(:or))
end
validates :group, presence: true, unless: :project
validates :project, presence: true, unless: :group
validates :title, presence: true, uniqueness: { scope: :project_id } validate :uniqueness_of_title, if: :title_changed?
validates :project, presence: true validate :milestone_type_check
validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? } validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? }
strip_attributes :title strip_attributes :title
...@@ -67,6 +82,14 @@ class Milestone < ActiveRecord::Base ...@@ -67,6 +82,14 @@ class Milestone < ActiveRecord::Base
where(t[:title].matches(pattern).or(t[:description].matches(pattern))) where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end end
def filter_by_state(milestones, state)
case state
when 'closed' then milestones.closed
when 'all' then milestones
else milestones.active
end
end
end end
def self.reference_prefix def self.reference_prefix
...@@ -142,6 +165,8 @@ class Milestone < ActiveRecord::Base ...@@ -142,6 +165,8 @@ class Milestone < ActiveRecord::Base
# Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1" # Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1"
# #
def to_reference(from_project = nil, format: :iid, full: false) def to_reference(from_project = nil, format: :iid, full: false)
return if is_group_milestone?
format_reference = milestone_format_reference(format) format_reference = milestone_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}" reference = "#{self.class.reference_prefix}#{format_reference}"
...@@ -156,6 +181,10 @@ class Milestone < ActiveRecord::Base ...@@ -156,6 +181,10 @@ class Milestone < ActiveRecord::Base
id id
end end
def for_display
self
end
def can_be_closed? def can_be_closed?
active? && issues.opened.count.zero? active? && issues.opened.count.zero?
end end
...@@ -168,8 +197,45 @@ class Milestone < ActiveRecord::Base ...@@ -168,8 +197,45 @@ class Milestone < ActiveRecord::Base
write_attribute(:title, sanitize_title(value)) if value.present? write_attribute(:title, sanitize_title(value)) if value.present?
end end
def safe_title
title.to_slug.normalize.to_s
end
def parent
group || project
end
def is_group_milestone?
group_id.present?
end
def is_project_milestone?
project_id.present?
end
private private
# Milestone titles must be unique across project milestones and group milestones
def uniqueness_of_title
if project
relation = Milestone.for_projects_and_groups([project_id], [project.group&.id])
elsif group
project_ids = group.projects.map(&:id)
relation = Milestone.for_projects_and_groups(project_ids, [group.id])
end
title_exists = relation.find_by_title(title)
errors.add(:title, "already being used for another group or project milestone.") if title_exists
end
# Milestone should be either a project milestone or a group milestone
def milestone_type_check
if group_id && project_id
field = project_id_changed? ? :project_id : :group_id
errors.add(field, "milestone should belong either to a project or a group.")
end
end
def milestone_format_reference(format = :iid) def milestone_format_reference(format = :iid)
raise ArgumentError, 'Unknown format' unless [:iid, :name].include?(format) raise ArgumentError, 'Unknown format' unless [:iid, :name].include?(format)
......
...@@ -535,6 +535,11 @@ class Project < ActiveRecord::Base ...@@ -535,6 +535,11 @@ class Project < ActiveRecord::Base
ProjectCacheWorker.perform_async(self.id) ProjectCacheWorker.perform_async(self.id)
end end
remove_import_data
end
# This method is overriden in EE::Project model
def remove_import_data
import_data&.destroy import_data&.destroy
end end
......
class GitlabIssueTrackerService < IssueTrackerService class GitlabIssueTrackerService < IssueTrackerService
include Gitlab::Routing.url_helpers include Gitlab::Routing
validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated?
......
class JiraService < IssueTrackerService class JiraService < IssueTrackerService
include Gitlab::Routing.url_helpers include Gitlab::Routing
validates :url, url: true, presence: true, if: :activated? validates :url, url: true, presence: true, if: :activated?
validates :api_url, url: true, allow_blank: true validates :api_url, url: true, allow_blank: true
......
module ChatNames module ChatNames
class AuthorizeUserService class AuthorizeUserService
include Gitlab::Routing.url_helpers include Gitlab::Routing
def initialize(service, params) def initialize(service, params)
@service = service @service = service
......
...@@ -4,8 +4,11 @@ class IssuableBaseService < BaseService ...@@ -4,8 +4,11 @@ class IssuableBaseService < BaseService
private private
def create_milestone_note(issuable) def create_milestone_note(issuable)
milestone = issuable.milestone
return if milestone && milestone.is_group_milestone?
SystemNoteService.change_milestone( SystemNoteService.change_milestone(
issuable, issuable.project, current_user, issuable.milestone) issuable, issuable.project, current_user, milestone)
end end
def create_labels_note(issuable, old_labels) def create_labels_note(issuable, old_labels)
...@@ -91,10 +94,12 @@ class IssuableBaseService < BaseService ...@@ -91,10 +94,12 @@ class IssuableBaseService < BaseService
milestone_id = params[:milestone_id] milestone_id = params[:milestone_id]
return unless milestone_id return unless milestone_id
if milestone_id == IssuableFinder::NONE || params[:milestone_id] = '' if milestone_id == IssuableFinder::NONE
project.milestones.find_by(id: milestone_id).nil?
params[:milestone_id] = '' milestone =
end Milestone.for_projects_and_groups([project.id], [project.group&.id]).find_by_id(milestone_id)
params[:milestone_id] = '' unless milestone
end end
def filter_labels def filter_labels
......
...@@ -61,8 +61,18 @@ module Issues ...@@ -61,8 +61,18 @@ module Issues
end end
def cloneable_milestone_id def cloneable_milestone_id
@new_project.milestones title = @old_issue.milestone&.title
.find_by(title: @old_issue.milestone.try(:title)).try(:id) return unless title
if @new_project.group && can?(current_user, :read_group, @new_project.group)
group_id = @new_project.group.id
end
params =
{ title: title, project_ids: @new_project.id, group_ids: group_id }
milestones = MilestonesFinder.new(params).execute
milestones.first&.id
end end
def rewrite_notes def rewrite_notes
......
...@@ -36,11 +36,12 @@ module MergeRequests ...@@ -36,11 +36,12 @@ module MergeRequests
# target branch manually # target branch manually
def close_merge_requests def close_merge_requests
commit_ids = @commits.map(&:id) commit_ids = @commits.map(&:id)
merge_requests = @project.merge_requests.opened.where(target_branch: @branch_name).to_a merge_requests = @project.merge_requests.preload(:merge_request_diff).opened.where(target_branch: @branch_name).to_a
merge_requests = merge_requests.select(&:diff_head_commit) merge_requests = merge_requests.select(&:diff_head_commit)
merge_requests = merge_requests.select do |merge_request| merge_requests = merge_requests.select do |merge_request|
commit_ids.include?(merge_request.diff_head_sha) commit_ids.include?(merge_request.diff_head_sha) &&
merge_request.merge_request_diff.state != 'empty'
end end
filter_merge_requests(merge_requests).each do |merge_request| filter_merge_requests(merge_requests).each do |merge_request|
......
module Milestones module Milestones
class BaseService < ::BaseService class BaseService < ::BaseService
# Parent can either a group or a project
attr_accessor :parent, :current_user, :params
def initialize(parent, user, params = {})
@parent, @current_user, @params = parent, user, params.dup
super
end
end end
end end
module Milestones module Milestones
class CloseService < Milestones::BaseService class CloseService < Milestones::BaseService
def execute(milestone) def execute(milestone)
if milestone.close if milestone.close && milestone.is_project_milestone?
event_service.close_milestone(milestone, current_user) event_service.close_milestone(milestone, current_user)
end end
......
module Milestones module Milestones
class CreateService < Milestones::BaseService class CreateService < Milestones::BaseService
def execute def execute
milestone = project.milestones.new(params) milestone = parent.milestones.new(params)
if milestone.save if milestone.save && milestone.is_project_milestone?
event_service.open_milestone(milestone, current_user) event_service.open_milestone(milestone, current_user)
end end
......
module Milestones module Milestones
class DestroyService < Milestones::BaseService class DestroyService < Milestones::BaseService
def execute(milestone) def execute(milestone)
return unless milestone.is_project_milestone?
Milestone.transaction do Milestone.transaction do
update_params = { milestone: nil } update_params = { milestone: nil }
milestone.issues.each do |issue| milestone.issues.each do |issue|
Issues::UpdateService.new(project, current_user, update_params).execute(issue) Issues::UpdateService.new(parent, current_user, update_params).execute(issue)
end end
milestone.merge_requests.each do |merge_request| milestone.merge_requests.each do |merge_request|
MergeRequests::UpdateService.new(project, current_user, update_params).execute(merge_request) MergeRequests::UpdateService.new(parent, current_user, update_params).execute(merge_request)
end end
event_service.destroy_milestone(milestone, current_user) event_service.destroy_milestone(milestone, current_user)
......
module Milestones module Milestones
class ReopenService < Milestones::BaseService class ReopenService < Milestones::BaseService
def execute(milestone) def execute(milestone)
if milestone.activate if milestone.activate && milestone.is_project_milestone?
event_service.reopen_milestone(milestone, current_user) event_service.reopen_milestone(milestone, current_user)
end end
......
...@@ -5,9 +5,9 @@ module Milestones ...@@ -5,9 +5,9 @@ module Milestones
case state case state
when 'activate' when 'activate'
Milestones::ReopenService.new(project, current_user, {}).execute(milestone) Milestones::ReopenService.new(parent, current_user, {}).execute(milestone)
when 'close' when 'close'
Milestones::CloseService.new(project, current_user, {}).execute(milestone) Milestones::CloseService.new(parent, current_user, {}).execute(milestone)
end end
if params.present? if params.present?
......
.row.blank-state.clearfix
.col-md-1.col-md-offset-3.blank-state-icon
= custom_icon("add_new_user", size: 50)
.col-md-5.blank-state-body
%h3.blank-state-title
Add user
%p.blank-state-text
Add your team members and others to GitLab.
= link_to new_admin_user_path, class: "btn btn-new" do
New user
.row.blank-state.clearfix
.col-md-1.col-md-offset-3.blank-state-icon
= custom_icon("configure_server", size: 50)
.col-md-5.blank-state-body
%h3.blank-state-title
Configure GitLab
%p.blank-state-text
Make adjustments to how your GitLab instance is set up.
= link_to admin_root_path, class: "btn btn-new" do
Configure
- if current_user.can_create_group?
.row.blank-state.clearfix
.col-md-1.col-md-offset-3.blank-state-icon
= custom_icon("add_new_group", size: 50)
.col-md-5.blank-state-body
%h3.blank-state-title
Create a group
%p.blank-state-text
Groups are a great way to organise projects and people.
= link_to new_group_path, class: "btn btn-new" do
New group
- public_project_count = ProjectsFinder.new(current_user: current_user).execute.count
- if current_user.can_create_group?
.row.blank-state.clearfix
.col-md-1.col-md-offset-3.blank-state-icon
= custom_icon("add_new_group", size: 50)
.col-md-5.blank-state-body
%h3.blank-state-title
Create a group for several dependent projects.
%p.blank-state-text
Groups are the best way to manage projects and members.
= link_to new_group_path, class: "btn btn-new" do
New group
.row.blank-state.clearfix
.col-md-1.col-md-offset-3.blank-state-icon
= custom_icon("add_new_project", size: 50)
.col-md-5.blank-state-body
%h3.blank-state-title
Create a project
%p.blank-state-text
- if current_user.can_create_project?
You don't have access to any projects right now.
You can create up to
%strong= number_with_delimiter(current_user.projects_limit)
= succeed "." do
= "project".pluralize(current_user.projects_limit)
- else
If you are added to a project, it will be displayed here.
- if current_user.can_create_project?
= link_to new_project_path, class: "btn btn-new" do
New project
- if public_project_count > 0
.row.blank-state.clearfix
.col-md-1.col-md-offset-3.blank-state-icon
= custom_icon("globe", size: 50)
.col-md-5.blank-state-body
%h3.blank-state-title
Explore public projects
%p.blank-state-text
There are
= number_with_delimiter(public_project_count)
public projects on this server.
Public projects are an easy way to allow
everyone to have read-only access.
= link_to trending_explore_projects_path, class: "btn btn-new" do
Browse projects
- publicish_project_count = ProjectsFinder.new(current_user: current_user).execute.count .row.blank-state-parent-container
.blank-state.blank-state-welcome .section-container
%h2.blank-state-welcome-title .container.section-body.section-welcome{ class: "#{ 'section-admin-welcome' if current_user.admin? }" }
Welcome to GitLab .blank-state.blank-state-welcome
%p.blank-state-text %h2.blank-state-welcome-title
Code, test, and deploy together Welcome to GitLab
%p.blank-state-text
- if current_user.can_create_group? Code, test, and deploy together
.blank-state - if current_user.admin?
.blank-state-icon = render "blank_state_admin_welcome"
= custom_icon("group", size: 50) - else
%h3.blank-state-title = render "blank_state_welcome"
You can create a group for several dependent projects.
%p.blank-state-text
Groups are the best way to manage projects and members.
= link_to new_group_path, class: "btn btn-new" do
New group
.blank-state
.blank-state-icon
= custom_icon("project", size: 50)
%h3.blank-state-title
You don't have access to any projects right now
%p.blank-state-text
- if current_user.can_create_project?
You can create up to
%strong= number_with_delimiter(current_user.projects_limit)
= succeed "." do
= "project".pluralize(current_user.projects_limit)
- else
If you are added to a project, it will be displayed here.
- if current_user.can_create_project?
= link_to new_project_path, class: "btn btn-new" do
New project
- if publicish_project_count > 0
.blank-state
.blank-state-icon
= icon("globe")
%h3.blank-state-title
There are
= number_with_delimiter(publicish_project_count)
public projects on this server.
%p.blank-state-text
Public projects are an easy way to allow everyone to have read-only access.
= link_to trending_explore_projects_path, class: "btn btn-new" do
Browse projects
= form_for [@group, @milestone], html: { class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
.row
= form_errors(@milestone)
.col-md-6
.form-group
= f.label :title, "Title", class: "control-label"
.col-sm-10
= f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true
.form-group.milestone-description
= f.label :description, "Description", class: "control-label"
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: '' } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
.clearfix
.error-alert
= render "shared/milestones/form_dates", f: f
.form-actions
- if @milestone.new_record?
= f.submit 'Create milestone', class: "btn-create btn"
= link_to "Cancel", group_milestones_path(@group), class: "btn btn-cancel"
- else
= f.submit 'Update milestone', class: "btn-create btn"
= link_to "Cancel", group_milestone_path(@group, @milestone), class: "btn btn-cancel"
= render 'shared/milestones/milestone', = render 'shared/milestones/milestone',
milestone_path: group_milestone_path(@group, milestone.safe_title, title: milestone.title), milestone_path: group_milestone_route(milestone),
issues_path: issues_group_path(@group, milestone_title: milestone.title), issues_path: issues_group_path(@group, milestone_title: milestone.title),
merge_requests_path: merge_requests_group_path(@group, milestone_title: milestone.title), merge_requests_path: merge_requests_group_path(@group, milestone_title: milestone.title),
milestone: milestone milestone: milestone
- page_title "Milestones"
- render "header_title"
%h3.page-title
Edit Milestone
= render "form"
...@@ -9,11 +9,6 @@ ...@@ -9,11 +9,6 @@
= link_to new_group_milestone_path(@group), class: "btn btn-new" do = link_to new_group_milestone_path(@group), class: "btn btn-new" do
New milestone New milestone
.row-content-block
Only milestones from
%strong= @group.name
group are listed here.
.milestones .milestones
%ul.content-list %ul.content-list
- if @milestones.blank? - if @milestones.blank?
......
...@@ -4,40 +4,4 @@ ...@@ -4,40 +4,4 @@
%h3.page-title %h3.page-title
New Milestone New Milestone
%p.light = render "form"
This will create milestone in every selected project
%hr
= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
.row
- if @milestone.errors.any?
#error_explanation
.alert.alert-danger
%ul
- @milestone.errors.full_messages.each do |msg|
%li
= msg
.col-md-6
.form-group
= f.label :title, "Title", class: "control-label"
.col-sm-10
= f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true
.form-group.milestone-description
= f.label :description, "Description", class: "control-label"
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: '' } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
.clearfix
.error-alert
.form-group
= f.label :projects, "Projects", class: "control-label"
.col-sm-10
= f.collection_select :project_ids, @group.projects.non_archived, :id, :name,
{ selected: @group.projects.non_archived.pluck(:id) }, required: true, multiple: true, class: 'select2'
= render "shared/milestones/form_dates", f: f
.form-actions
= f.submit 'Create milestone', class: "btn-create btn"
= link_to "Cancel", group_milestones_path(@group), class: "btn btn-cancel"
= render "header_title" = render "header_title"
= render 'shared/milestones/top', milestone: @milestone, group: @group = render 'shared/milestones/top', milestone: @milestone, group: @group
= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true = render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true if @milestone.is_legacy_group_milestone?
= render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 102 = render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 102
...@@ -12,10 +12,12 @@ ...@@ -12,10 +12,12 @@
.content-wrapper{ class: "#{(layout_nav_class unless show_new_nav?)}" } .content-wrapper{ class: "#{(layout_nav_class unless show_new_nav?)}" }
.alert-wrapper .alert-wrapper
= render "layouts/broadcast" = render "layouts/broadcast"
- if show_new_nav?
- if content_for?(:new_global_flash)
= yield :new_global_flash
= render "layouts/nav/breadcrumbs"
= render "layouts/flash" = render "layouts/flash"
= yield :flash_message = yield :flash_message
- if show_new_nav?
= render "layouts/nav/breadcrumbs"
%div{ class: "#{(container_class unless @no_container)} #{@content_class}" } %div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
.content{ id: "content-body" } .content{ id: "content-body" }
= yield = yield
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
= icon('wrench') = icon('wrench')
.project-title Admin Area .project-title Admin Area
%ul.sidebar-top-level-items %ul.sidebar-top-level-items
= nav_link(controller: %w(dashboard admin projects users groups builds runners cohorts), html_options: {class: 'home'}) do = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
%span %span
Overview Overview
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
= link_to admin_groups_path, title: 'Groups' do = link_to admin_groups_path, title: 'Groups' do
%span %span
Groups Groups
= nav_link path: 'builds#index' do = nav_link path: 'jobs#index' do
= link_to admin_jobs_path, title: 'Jobs' do = link_to admin_jobs_path, title: 'Jobs' do
%span %span
Jobs Jobs
......
.nav-sidebar .nav-sidebar
= link_to group_path(@group), title: 'Group', class: 'context-header' do = link_to group_path(@group), title: @group.name, class: 'context-header' do
.avatar-container.s40.group-avatar .avatar-container.s40.group-avatar
= image_tag group_icon(@group), class: "avatar s40 avatar-tile" = image_tag group_icon(@group), class: "avatar s40 avatar-tile"
.group-title .group-title
......
.nav-sidebar .nav-sidebar
- can_edit = can?(current_user, :admin_project, @project) - can_edit = can?(current_user, :admin_project, @project)
= link_to project_path(@project), title: 'Project', class: 'context-header' do = link_to project_path(@project), title: @project.name, class: 'context-header' do
.avatar-container.s40.project-avatar .avatar-container.s40.project-avatar
= project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile') = project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile')
.project-title .project-title
......
...@@ -57,16 +57,17 @@ ...@@ -57,16 +57,17 @@
%span %span
Snippets Snippets
- if project_nav_tab? :project_members
= nav_link(controller: :project_members) do
= link_to project_project_members_path(@project), title: 'Members', class: 'shortcuts-members' do
%span
Members
- if project_nav_tab? :settings - if project_nav_tab? :settings
= nav_link(path: %w[projects#edit members#show integrations#show services#edit repository#show ci_cd#show pages#show]) do = nav_link(path: %w[projects#edit members#show integrations#show services#edit repository#show ci_cd#show pages#show]) do
= link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do
%span %span
Settings Settings
- else
= nav_link(path: %w[members#show]) do
= link_to project_settings_members_path(@project), title: 'Settings', class: 'shortcuts-tree' do
%span
Settings
-# Shortcut to Project > Activity -# Shortcut to Project > Activity
%li.hidden %li.hidden
......
- @no_container = true - @no_container = true
- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message
= content_for :flash_message do = content_for flash_message_container do
- if current_user && can?(current_user, :download_code, @project) - if current_user && can?(current_user, :download_code, @project)
= render 'shared/no_ssh' = render 'shared/no_ssh'
= render 'shared/no_password' = render 'shared/no_password'
......
...@@ -33,24 +33,23 @@ ...@@ -33,24 +33,23 @@
.dropdown-menu.dropdown-menu-align-right.hidden-lg .dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul %ul
- if can_update_issue - if can_update_issue
%li %li= link_to 'Edit', edit_project_issue_path(@project, @issue)
= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'issuable-edit' - unless current_user == @issue.author
%li %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' - if can_update_issue
%li %li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' %li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- if can_report_spam - if can_report_spam
%li %li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
- if can_update_issue || can_report_spam - if can_update_issue || can_report_spam
%li.divider %li.divider
%li %li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
- if can_update_issue - if can_update_issue
= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' = link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue
- if can_report_spam - if can_report_spam
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam' = link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
= link_to new_project_issue_path(@project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do = link_to new_project_issue_path(@project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
......
- can_update_merge_request = can?(current_user, :update_merge_request, @merge_request)
- if @merge_request.closed_without_fork? - if @merge_request.closed_without_fork?
.alert.alert-danger .alert.alert-danger
%p The source project of this merge request has been removed. %p The source project of this merge request has been removed.
...@@ -15,21 +17,24 @@ ...@@ -15,21 +17,24 @@
.issuable-meta .issuable-meta
= issuable_meta(@merge_request, @project, "Merge request") = issuable_meta(@merge_request, @project, "Merge request")
- if can?(current_user, :update_merge_request, @merge_request) .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" } } Options
Options = icon('caret-down')
= icon('caret-down') .dropdown-menu.dropdown-menu-align-right.hidden-lg
.dropdown-menu.dropdown-menu-align-right.hidden-lg %ul
%ul - if can_update_merge_request
%li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit'
- unless current_user == @merge_request.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request))
- if can_update_merge_request
%li{ class: merge_request_button_visibility(@merge_request, true) } %li{ class: merge_request_button_visibility(@merge_request, true) }
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request' = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
%li{ class: merge_request_button_visibility(@merge_request, false) } %li{ class: merge_request_button_visibility(@merge_request, false) }
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
%li
= link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: 'issuable-edit' - if can_update_merge_request
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{merge_request_button_visibility(@merge_request, true)}", title: 'Close merge request' = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit"
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen reopen-mr-link #{merge_request_button_visibility(@merge_request, false)}", title: 'Reopen merge request'
= link_to edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit" do = render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request
Edit
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
%strong %strong
#{@project.name} #{@project.name}
%span.badge= @project_members.total_count %span.badge= @project_members.total_count
= form_tag project_settings_members_path(@project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do = form_tag project_project_members_path(@project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
.form-group .form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
%button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
......
...@@ -12,4 +12,4 @@ ...@@ -12,4 +12,4 @@
.form-actions .form-actions
= button_tag 'Import project members', class: "btn btn-create" = button_tag 'Import project members', class: "btn btn-create"
= link_to "Cancel", project_settings_members_path(@project), class: "btn btn-cancel" = link_to "Cancel", project_project_members_path(@project), class: "btn btn-cancel"
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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