diff options
Diffstat (limited to 'app')
73 files changed, 568 insertions, 398 deletions
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 1be9df19c81..6a008112203 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -2,6 +2,7 @@ import './lib/utils/url_utility'; import FilesCommentButton from './files_comment_button'; +import SingleFileDiff from './single_file_diff'; const UNFOLD_COUNT = 20; let isBound = false; @@ -10,7 +11,11 @@ class Diff { constructor() { const $diffFile = $('.files .diff-file'); - $diffFile.singleFileDiff(); + $diffFile.each((index, file) => { + if (!$.data(file, 'singleFileDiff')) { + $.data(file, 'singleFileDiff', new SingleFileDiff(file)); + } + }); FilesCommentButton.init($diffFile); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 81267c68cfc..ae19592ecbe 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -1,17 +1,13 @@ /* 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 IssuableIndex */ /* global ShortcutsIssuable */ -/* global ZenMode */ /* global Milestone */ /* global IssuableForm */ /* global LabelsSelect */ /* global MilestoneSelect */ /* global Commit */ /* global NotificationsForm */ -/* global TreeView */ /* global NotificationsDropdown */ /* global GroupAvatar */ /* global LineHighlighter */ @@ -25,7 +21,6 @@ /* global ProjectAvatar */ /* global CompareAutocomplete */ /* global ProjectNew */ -/* global Star */ /* global ProjectShow */ /* global Labels */ /* global Shortcuts */ @@ -54,6 +49,15 @@ import UsersSelect from './users_select'; import RefSelectDropdown from './ref_select_dropdown'; import GfmAutoComplete from './gfm_auto_complete'; 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 initExperimentalFlags from './experimental_flags'; import OAuthRememberMe from './oauth_remember_me'; @@ -128,7 +132,7 @@ import PerformanceBar from './performance_bar'; break; case 'sessions:new': new UsernameValidator(); - new ActiveTabMemoizer(); + new SigninTabsMemoizer(); new OAuthRememberMe({ container: $(".omniauth-container") }).bindEvents(); break; case 'projects:boards:show': @@ -164,7 +168,7 @@ import PerformanceBar from './performance_bar'; new UsersSelect(); break; case 'dashboard:todos:index': - new gl.Todos(); + new Todos(); break; case 'dashboard:projects:index': case 'dashboard:projects:starred': @@ -318,7 +322,7 @@ import PerformanceBar from './performance_bar'; new gl.Members(); new UsersSelect(); break; - case 'projects:settings:members:show': + case 'projects:project_members:index': new gl.MemberExpirationDate('.js-access-expiration-date-groups'); new GroupsSelect(); new gl.MemberExpirationDate(); @@ -380,7 +384,7 @@ import PerformanceBar from './performance_bar'; new BlobViewer(); break; case 'help:index': - gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); + VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); break; case 'search:show': new Search(); @@ -433,7 +437,7 @@ import PerformanceBar from './performance_bar'; new Admin(); switch (path[1]) { case 'cohorts': - new gl.UsagePing(); + new UsagePing(); break; case 'groups': new UsersSelect(); @@ -485,7 +489,7 @@ import PerformanceBar from './performance_bar'; new NotificationsDropdown(); break; case 'wikis': - new gl.Wikis(); + new Wikis(); shortcut_handler = new ShortcutsWiki(); new ZenMode(); new gl.GLForm($('.wiki-form'), true); diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 92f6f0d4117..9ac1325fc95 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -1,12 +1,12 @@ /* 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 ZenMode */ /* global Autosave */ /* global dateFormat */ /* global Pikaday */ import UsersSelect from './users_select'; import GfmAutoComplete from './gfm_auto_complete'; +import ZenMode from './zen_mode'; (function() { this.IssuableForm = (function() { diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 0860e237ce1..7490cd31f58 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -4,13 +4,13 @@ import 'vendor/jquery.waitforimages'; import '~/lib/utils/text_utility'; import './flash'; -import './task_list'; +import TaskList from './task_list'; import CreateMergeRequestDropdown from './create_merge_request_dropdown'; class Issue { constructor() { if ($('a.btn-close').length) { - this.taskList = new gl.TaskList({ + this.taskList = new TaskList({ dataType: 'issue', fieldName: 'description', selector: '.detail-page-description', diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index 43db66c8e08..48bad8f1e68 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -1,5 +1,6 @@ <script> import animateMixin from '../mixins/animate'; + import TaskList from '../../task_list'; export default { mixins: [animateMixin], @@ -46,7 +47,7 @@ if (this.canUpdate) { // eslint-disable-next-line no-new - new gl.TaskList({ + new TaskList({ dataType: 'issue', fieldName: 'description', selector: '.detail-page-description', diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index fe752d95b90..892b3fab1c6 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -143,26 +143,13 @@ import './render_math'; import './right_sidebar'; import './search'; import './search_autocomplete'; -import './signin_tabs_memoizer'; -import './single_file_diff'; import './smart_interval'; import './snippets_list'; import './star'; import './subscription'; import './subscription_select'; import './syntax_highlight'; -import './task_list'; -import './todos'; -import './tree'; -import './usage_ping'; import './user'; -import './user_tabs'; -import './username_validator'; -import './users_select'; -import './version_check_image'; -import './visibility_select'; -import './wikis'; -import './zen_mode'; // eslint-disable-next-line global-require, import/no-commonjs if (process.env.NODE_ENV !== 'production') require('./test_utils/'); diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index f93feeec1c2..7ba9efc5387 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -2,7 +2,7 @@ /* global MergeRequestTabs */ import 'vendor/jquery.waitforimages'; -import './task_list'; +import TaskList from './task_list'; import './merge_request_tabs'; (function() { @@ -25,7 +25,7 @@ import './merge_request_tabs'; this.initMRBtnListeners(); this.initCommitMessageListeners(); if ($("a.btn-close").length) { - this.taskList = new gl.TaskList({ + this.taskList = new TaskList({ dataType: 'merge_request', fieldName: 'description', selector: '.detail-page-description', diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 555b8c8a65c..1a68c5bca00 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -21,7 +21,7 @@ import CommentTypeToggle from './comment_type_toggle'; import loadAwardsHandler from './awards_handler'; import './autosave'; import './dropzone_input'; -import './task_list'; +import TaskList from './task_list'; window.autosize = autosize; window.Dropzone = Dropzone; @@ -71,7 +71,7 @@ export default class Notes { this.addBinding(); this.setPollingInterval(); this.setupMainTargetNoteForm(); - this.taskList = new gl.TaskList({ + this.taskList = new TaskList({ dataType: 'note', fieldName: 'note', selector: '.notes' diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js index c0f757269cb..fd89a1a85c3 100644 --- a/app/assets/javascripts/project_new.js +++ b/app/assets/javascripts/project_new.js @@ -1,5 +1,7 @@ /* 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) { $elm.addClass('highlight-changes'); setTimeout(() => $elm.removeClass('highlight-changes'), 10); @@ -30,7 +32,7 @@ function highlightChanges($elm) { ProjectNew.prototype.initVisibilitySelect = function() { const visibilityContainer = document.querySelector('.js-visibility-select'); if (!visibilityContainer) return; - const visibilitySelect = new gl.VisibilitySelect(visibilityContainer); + const visibilitySelect = new VisibilitySelect(visibilityContainer); visibilitySelect.init(); const $visibilitySelect = $(visibilityContainer).find('select'); diff --git a/app/assets/javascripts/signin_tabs_memoizer.js b/app/assets/javascripts/signin_tabs_memoizer.js index 3997a695d15..20255398047 100644 --- a/app/assets/javascripts/signin_tabs_memoizer.js +++ b/app/assets/javascripts/signin_tabs_memoizer.js @@ -6,7 +6,7 @@ import AccessorUtilities from './lib/utils/accessor'; * Memorize the last selected tab after reloading a page. * 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' } = {}) { this.currentTabKey = currentTabKey; this.tabSelector = tabSelector; @@ -51,5 +51,3 @@ class ActiveTabMemoizer { return window.localStorage.getItem(this.currentTabKey); } } - -window.ActiveTabMemoizer = ActiveTabMemoizer; diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 00d04ce0c33..4505a79a2df 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -2,18 +2,13 @@ import FilesCommentButton from './files_comment_button'; -window.SingleFileDiff = (function() { - var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER; +const WRAPPER = '<div class="diff-content"></div>'; +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>'; - - 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) { +export default class SingleFileDiff { + constructor(file) { this.file = file; this.toggleDiff = this.toggleDiff.bind(this); this.content = $('.diff-content', this.file); @@ -37,7 +32,7 @@ window.SingleFileDiff = (function() { }).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; this.isOpen = !this.isOpen; if (!this.isOpen && !this.hasError) { @@ -58,9 +53,9 @@ window.SingleFileDiff = (function() { this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right'); return this.getContentHTML(cb); } - }; + } - SingleFileDiff.prototype.getContentHTML = function(cb) { + getContentHTML(cb) { this.collapsedContent.hide(); this.loadingContent.show(); $.get(this.diffForPath, (function(_this) { @@ -84,15 +79,5 @@ window.SingleFileDiff = (function() { if (cb) cb(); }; })(this)); - }; - - return SingleFileDiff; -})(); - -$.fn.singleFileDiff = function() { - return this.each(function() { - if (!$.data(this, 'singleFileDiff')) { - return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this)); - } - }); -}; + } +} diff --git a/app/assets/javascripts/snippets_list.js b/app/assets/javascripts/snippets_list.js index da7b9e08447..3b6d999b1c3 100644 --- a/app/assets/javascripts/snippets_list.js +++ b/app/assets/javascripts/snippets_list.js @@ -1,9 +1,9 @@ -/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, max-len */ - -window.gl.SnippetsList = function() { - var $holder = $('.snippets-list-holder'); +function SnippetsList() { + const $holder = $('.snippets-list-holder'); $holder.find('.pagination').on('ajax:success', (e, data) => { $holder.replaceWith(data.html); }); -}; +} + +window.gl.SnippetsList = SnippetsList; diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index 840ae1edd9d..6d38124f1c1 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -1,8 +1,8 @@ /* 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 */ -window.Star = (function() { - function Star() { +export default class Star { + constructor() { $('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) { var $starIcon, $starSpan, $this, toggleStar; $this = $(this); @@ -23,6 +23,4 @@ window.Star = (function() { new Flash('Star toggle failed. Try again later.', 'alert'); }); } - - return Star; -})(); +} diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js index a48434181b6..37e39ce5477 100644 --- a/app/assets/javascripts/subscription_select.js +++ b/app/assets/javascripts/subscription_select.js @@ -1,7 +1,7 @@ /* 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() { - function SubscriptionSelect() { +class SubscriptionSelect { + constructor() { $('.js-subscription-event').each(function(i, el) { var fieldName; fieldName = $(el).data("field-name"); @@ -28,6 +28,6 @@ window.SubscriptionSelect = (function() { }); }); } +} - return SubscriptionSelect; -})(); +window.SubscriptionSelect = SubscriptionSelect; diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js index 419c458ff34..c39f569da5e 100644 --- a/app/assets/javascripts/task_list.js +++ b/app/assets/javascripts/task_list.js @@ -2,7 +2,7 @@ import 'deckar01-task_list'; -class TaskList { +export default class TaskList { constructor(options = {}) { this.selector = options.selector; this.dataType = options.dataType; @@ -48,6 +48,3 @@ class TaskList { }); } } - -window.gl = window.gl || {}; -window.gl.TaskList = TaskList; diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js index 7230946b484..cd305631c10 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/todos.js @@ -2,7 +2,7 @@ import UsersSelect from './users_select'; -class Todos { +export default class Todos { constructor() { this.initFilters(); this.bindEvents(); @@ -159,6 +159,3 @@ class Todos { } } } - -window.gl = window.gl || {}; -gl.Todos = Todos; diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js index 77ae6109bc6..7777ed1c3dc 100644 --- a/app/assets/javascripts/tree.js +++ b/app/assets/javascripts/tree.js @@ -1,7 +1,7 @@ -/* 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() { - function TreeView() { +export default class TreeView { + constructor() { this.initKeyNav(); // Code browser tree slider // Make the entire tree-item row clickable, but not if clicking another link (like a commit message) @@ -22,7 +22,7 @@ window.TreeView = (function() { $('span.log_loading:first').removeClass('hide'); } - TreeView.prototype.initKeyNav = function() { + initKeyNav() { var li, liSelected; li = $("tr.tree-item"); liSelected = null; @@ -60,7 +60,5 @@ window.TreeView = (function() { } } }); - }; - - return TreeView; -})(); + } +} diff --git a/app/assets/javascripts/usage_ping.js b/app/assets/javascripts/usage_ping.js index fd3af7d7ab6..2389056bd02 100644 --- a/app/assets/javascripts/usage_ping.js +++ b/app/assets/javascripts/usage_ping.js @@ -1,4 +1,4 @@ -function UsagePing() { +export default function UsagePing() { const usageDataUrl = $('.usage-data').data('endpoint'); $.ajax({ @@ -10,6 +10,3 @@ function UsagePing() { }, }); } - -window.gl = window.gl || {}; -window.gl.UsagePing = UsagePing; diff --git a/app/assets/javascripts/user.js b/app/assets/javascripts/user.js index 3ab9ef5408e..9ef94ac7616 100644 --- a/app/assets/javascripts/user.js +++ b/app/assets/javascripts/user.js @@ -1,6 +1,7 @@ /* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign */ import Cookies from 'js-cookie'; +import UserTabs from './user_tabs'; class User { constructor({ action }) { @@ -17,7 +18,7 @@ class User { } initTabs() { - return new window.gl.UserTabs({ + return new UserTabs({ parentEl: '.user-profile', action: this.action }); diff --git a/app/assets/javascripts/user_tabs.js b/app/assets/javascripts/user_tabs.js index be70f4cb4e2..f8e23c8624d 100644 --- a/app/assets/javascripts/user_tabs.js +++ b/app/assets/javascripts/user_tabs.js @@ -60,7 +60,7 @@ content on the Users#show page. </div> */ -class UserTabs { +export default class UserTabs { constructor ({ defaultAction, action, parentEl }) { this.loaded = {}; this.defaultAction = defaultAction || 'activity'; @@ -171,6 +171,3 @@ class UserTabs { return this.$parentEl.find('.nav-links .active a').data('action'); } } - -window.gl = window.gl || {}; -window.gl.UserTabs = UserTabs; diff --git a/app/assets/javascripts/username_validator.js b/app/assets/javascripts/username_validator.js index abe6c30f4f3..a348d69153c 100644 --- a/app/assets/javascripts/username_validator.js +++ b/app/assets/javascripts/username_validator.js @@ -8,7 +8,7 @@ const successMessageSelector = '.username .validation-success'; const pendingMessageSelector = '.username .validation-pending'; const invalidMessageSelector = '.username .gl-field-error'; -class UsernameValidator { +export default class UsernameValidator { constructor() { this.inputElement = $('#new_user_username'); this.inputDomElement = this.inputElement.get(0); @@ -129,5 +129,3 @@ class UsernameValidator { $inputErrorMessage.show(); } } - -window.UsernameValidator = UsernameValidator; diff --git a/app/assets/javascripts/version_check_image.js b/app/assets/javascripts/version_check_image.js index 88ba991af47..ec515e892c6 100644 --- a/app/assets/javascripts/version_check_image.js +++ b/app/assets/javascripts/version_check_image.js @@ -3,6 +3,3 @@ export default class VersionCheckImage { imageElement.off('error').on('error', () => imageElement.hide()); } } - -window.gl = window.gl || {}; -gl.VersionCheckImage = VersionCheckImage; diff --git a/app/assets/javascripts/visibility_select.js b/app/assets/javascripts/visibility_select.js index b6bbbaa0936..0c928d0d5f6 100644 --- a/app/assets/javascripts/visibility_select.js +++ b/app/assets/javascripts/visibility_select.js @@ -1,4 +1,4 @@ -class VisibilitySelect { +export default class VisibilitySelect { constructor(container) { if (!container) throw new Error('VisibilitySelect requires a container element as argument 1'); this.container = container; @@ -19,6 +19,3 @@ class VisibilitySelect { this.helpBlock.textContent = this.select.querySelector('option:checked').dataset.description; } } - -window.gl = window.gl || {}; -window.gl.VisibilitySelect = VisibilitySelect; diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js index 03d183ebd84..00676bcb0b3 100644 --- a/app/assets/javascripts/wikis.js +++ b/app/assets/javascripts/wikis.js @@ -1,10 +1,9 @@ -/* eslint-disable no-param-reassign */ /* global Breakpoints */ import 'vendor/jquery.nicescroll'; import './breakpoints'; -class Wikis { +export default class Wikis { constructor() { this.bp = Breakpoints.get(); this.sidebarEl = document.querySelector('.js-wiki-sidebar'); @@ -63,6 +62,3 @@ class Wikis { } } } - -window.gl = window.gl || {}; -window.gl.Wikis = Wikis; diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index 08f80735e93..99c7644e4d9 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -1,4 +1,4 @@ -/* 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 */ // Zen Mode (full screen) textarea @@ -35,8 +35,8 @@ window.Dropzone = Dropzone; // **Target** a.js-zen-leave // -window.ZenMode = (function() { - function ZenMode() { +export default class ZenMode { + constructor() { this.active_backdrop = null; this.active_textarea = null; $(document).on('click', '.js-zen-enter', function(e) { @@ -66,7 +66,7 @@ window.ZenMode = (function() { }); } - ZenMode.prototype.enter = function(backdrop) { + enter(backdrop) { Mousetrap.pause(); this.active_backdrop = $(backdrop); this.active_backdrop.addClass('fullscreen'); @@ -74,9 +74,9 @@ window.ZenMode = (function() { // Prevent a user-resized textarea from persisting to fullscreen this.active_textarea.removeAttr('style'); return this.active_textarea.focus(); - }; + } - ZenMode.prototype.exit = function() { + exit() { if (this.active_textarea) { Mousetrap.unpause(); this.active_textarea.closest('.zen-backdrop').removeClass('fullscreen'); @@ -85,13 +85,11 @@ window.ZenMode = (function() { this.active_backdrop = null; return Dropzone.forElement('.div-dropzone').enable(); } - }; + } - ZenMode.prototype.scrollTo = function(zen_area) { + scrollTo(zen_area) { return $.scrollTo(zen_area, 0, { offset: -150 }); - }; - - return ZenMode; -})(); + } +} diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb index 47d9ae350ae..c6b1e443de6 100644 --- a/app/controllers/concerns/membership_actions.rb +++ b/app/controllers/concerns/membership_actions.rb @@ -70,7 +70,7 @@ module MembershipActions def members_page_url if membershipable.is_a?(Project) - project_settings_members_path(membershipable) + project_project_members_path(membershipable) else polymorphic_url([membershipable, :members]) end diff --git a/app/controllers/dashboard/labels_controller.rb b/app/controllers/dashboard/labels_controller.rb index dd1d46a68c7..9dcb3a0eb6d 100644 --- a/app/controllers/dashboard/labels_controller.rb +++ b/app/controllers/dashboard/labels_controller.rb @@ -1,9 +1,14 @@ class Dashboard::LabelsController < Dashboard::ApplicationController def index - labels = LabelsFinder.new(current_user).execute - respond_to do |format| format.json { render json: LabelSerializer.new.represent_appearance(labels) } end end + + def labels + finder_params = { project_ids: projects.select(:id) } + labels = LabelsFinder.new(current_user, finder_params).execute + + GlobalLabel.build_collection(labels) + end end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 6b1d418fc9a..5c10d7bc261 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -2,13 +2,13 @@ class Groups::MilestonesController < Groups::ApplicationController include MilestoneActions before_action :group_projects - before_action :milestone, only: [:show, :update, :merge_requests, :participants, :labels] - before_action :authorize_admin_milestones!, only: [:new, :create, :update] + before_action :milestone, only: [:edit, :show, :update, :merge_requests, :participants, :labels] + before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update] def index respond_to do |format| 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]) end format.json do @@ -22,49 +22,41 @@ class Groups::MilestonesController < Groups::ApplicationController end def create - project_ids = params[:milestone][:project_ids].reject(&:blank?) - title = milestone_params[:title] + @milestone = Milestones::CreateService.new(group, current_user, milestone_params).execute - if create_milestones(project_ids) - redirect_to milestone_path(title) + if @milestone.persisted? + redirect_to milestone_path else - render_new_with_error(project_ids.empty?) + render "new" end end def show end - def update - @milestone.milestones.each do |milestone| - Milestones::UpdateService.new(milestone.project, current_user, milestone_params).execute(milestone) - end - - redirect_back_or_default(default: milestone_path(@milestone.title)) + def edit + render_404 if @milestone.is_legacy_group_milestone? end - private - - def create_milestones(project_ids) - return false unless project_ids.present? + def update + # Keep this compatible with legacy group milestones where we have to update + # all projects milestones states at once. + 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 - @projects.where(id: project_ids).each do |project| - Milestones::CreateService.new(project, current_user, milestone_params).execute - end + milestones.each do |milestone| + Milestones::UpdateService.new(milestone.parent, current_user, update_params).execute(milestone) end - true - rescue ActiveRecord::ActiveRecordError => e - flash.now[:alert] = "An error occurred while creating the milestone: #{e.message}" - false + redirect_to milestone_path end - def render_new_with_error(empty_project_ids) - @milestone = Milestone.new(milestone_params) - @milestone.errors.add(:base, "Please select at least one project.") if empty_project_ids - render :new - end + private def authorize_admin_milestones! return render_404 unless can?(current_user, :admin_milestones, group) @@ -74,16 +66,31 @@ class Groups::MilestonesController < Groups::ApplicationController params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event) end - def milestone_path(title) - group_milestone_path(@group, title.to_slug.to_s, title: title) + def milestone_path + 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 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 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 end end diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index 8fc614b414d..f59200d3b1f 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -22,7 +22,7 @@ class Projects::GroupLinksController < Projects::ApplicationController flash[:alert] = 'Please select a group.' end - redirect_to project_settings_members_path(project) + redirect_to project_project_members_path(project) end def update @@ -36,7 +36,7 @@ class Projects::GroupLinksController < Projects::ApplicationController respond_to do |format| format.html do - redirect_to project_settings_members_path(project), status: 302 + redirect_to project_project_members_path(project), status: 302 end format.js { head :ok } end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index a80562e77ce..c94384d2a1a 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -13,20 +13,16 @@ class Projects::MilestonesController < Projects::ApplicationController respond_to :html 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' - @milestones = @milestones.sort(@sort) + @milestones = milestones.sort(@sort) respond_to do |format| format.html do @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]) end format.json do @@ -45,12 +41,13 @@ class Projects::MilestonesController < Projects::ApplicationController end def show + @project_namespace = @project.namespace.becomes(Namespace) end def create @milestone = Milestones::CreateService.new(project, current_user, milestone_params).execute - if @milestone.save + if @milestone.valid? redirect_to project_milestone_path(@project, @milestone) else render "new" @@ -85,6 +82,18 @@ class Projects::MilestonesController < Projects::ApplicationController 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 @milestone ||= @project.milestones.find_by!(iid: params[:id]) end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 57a6686f66c..f8ff7413b53 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -6,8 +6,23 @@ class Projects::ProjectMembersController < Projects::ApplicationController before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access] def index - sort = params[:sort].presence || sort_value_name - redirect_to project_settings_members_path(@project, sort: sort) + @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 def update @@ -19,7 +34,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController end 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]) @@ -42,7 +57,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController return render_404 end - redirect_to(project_settings_members_path(project), + redirect_to(project_project_members_path(project), notice: notice) end diff --git a/app/controllers/projects/settings/members_controller.rb b/app/controllers/projects/settings/members_controller.rb deleted file mode 100644 index 54f9dceddef..00000000000 --- a/app/controllers/projects/settings/members_controller.rb +++ /dev/null @@ -1,27 +0,0 @@ -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 diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 7bc2117f61e..d81e9ed17d4 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -147,9 +147,17 @@ class IssuableFinder @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 Milestone.none end @@ -331,11 +339,6 @@ class IssuableFinder items = items.left_joins_milestones.where('milestones.start_date <= NOW()') else 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 diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb index 630c73c2a94..23c42a5f662 100644 --- a/app/finders/milestones_finder.rb +++ b/app/finders/milestones_finder.rb @@ -1,12 +1,55 @@ +# 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 - def execute(projects, params) - milestones = Milestone.of_projects(projects) - milestones = milestones.reorder("due_date ASC") - - case params[:state] - when 'closed' then milestones.closed - when 'all' then milestones - else milestones.active + attr_reader :params, :project_ids, :group_ids + + def initialize(params = {}) + @project_ids = Array(params[:project_ids]) + @group_ids = Array(params[:group_ids]) + @params = params + end + + 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 + items.reorder('due_date ASC') end end end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index b5f4bbe97dc..2e36d0fdb5a 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -97,7 +97,7 @@ module GitlabRoutingHelper ## Members def project_members_url(project, *args) - project_project_members_url(project) + project_project_members_url(project, *args) end def project_member_path(project_member, *args) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 8c7851dcfc2..f8860bfee99 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -54,8 +54,10 @@ module MilestonesHelper def milestone_class_for_state(param, check, match_blank_param = false) if match_blank_param 'active' if param.blank? || param == check + elsif param == check + 'active' else - 'active' if param == check + check end end @@ -147,4 +149,14 @@ module MilestonesHelper labels_dashboard_milestone_path(milestone, title: milestone.title, format: :json) 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 diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 5022b291f7f..25969adb649 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -267,15 +267,15 @@ module ProjectsHelper def tab_ability_map { - environments: :read_environment, - milestones: :read_milestone, - snippets: :read_project_snippet, - settings: :admin_project, - builds: :read_build, - labels: :read_label, - issues: :read_issue, - team: :read_project_member, - wiki: :read_wiki + environments: :read_environment, + milestones: :read_milestone, + snippets: :read_project_snippet, + settings: :admin_project, + builds: :read_build, + labels: :read_label, + issues: :read_issue, + project_members: :read_project_member, + wiki: :read_wiki } end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 8c44f4b0934..fd7ab59ce64 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -75,7 +75,7 @@ module SearchHelper { 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: "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) } ] else diff --git a/app/models/concerns/internal_id.rb b/app/models/concerns/internal_id.rb index 5382dde6765..67a0adfcd56 100644 --- a/app/models/concerns/internal_id.rb +++ b/app/models/concerns/internal_id.rb @@ -8,7 +8,8 @@ module InternalId def set_iid 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? max_iid = records.maximum(:iid) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 23cb85600da..13fe9d09c69 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -30,6 +30,7 @@ module Issuable belongs_to :updated_by, class_name: "User" belongs_to :last_edited_by, class_name: 'User' belongs_to :milestone + has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do # rubocop:disable Cop/ActiveRecordDependent def authors_loaded? # We check first if we're loaded to not load unnecessarily. diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 01599ce49c6..f0998465822 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -70,6 +70,22 @@ module Milestoneish due_date && due_date.past? 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 def count_issues_by_state(user) diff --git a/app/models/dashboard_milestone.rb b/app/models/dashboard_milestone.rb index 646c1e5ce1a..fac7c5e5c85 100644 --- a/app/models/dashboard_milestone.rb +++ b/app/models/dashboard_milestone.rb @@ -2,4 +2,8 @@ class DashboardMilestone < GlobalMilestone def issues_finder_params { authorized_only: true } end + + def is_dashboard_milestone? + true + end end diff --git a/app/models/global_label.rb b/app/models/global_label.rb index 698a7bbd327..2a1b7564962 100644 --- a/app/models/global_label.rb +++ b/app/models/global_label.rb @@ -2,7 +2,7 @@ class GlobalLabel attr_accessor :title, :labels alias_attribute :name, :title - delegate :color, :description, to: :@first_label + delegate :color, :text_color, :description, to: :@first_label def for_display @first_label diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index 538615130a7..c0864769314 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -2,6 +2,7 @@ class GlobalMilestone include Milestoneish EPOCH = DateTime.parse('1970-01-01') + STATE_COUNT_HASH = { opened: 0, closed: 0, all: 0 }.freeze attr_accessor :title, :milestones alias_attribute :name, :title @@ -11,7 +12,10 @@ class GlobalMilestone end 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_relation = Milestone.where(id: grouped.map(&:id)) @@ -28,13 +32,42 @@ class GlobalMilestone new(title, child_milestones) end - def self.states_count(projects) - relation = MilestonesFinder.new.execute(projects, state: 'all') - milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count + def self.states_count(projects, group = nil) + legacy_group_milestones_count = legacy_group_milestone_states_count(projects) + 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') - closed = count_by_state(milestones_by_state_and_title, 'closed') - all = milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count + opened = count_by_state(project_milestones_by_state_and_title, 'active') + closed = count_by_state(project_milestones_by_state_and_title, 'closed') + all = project_milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count { opened: opened, diff --git a/app/models/group.rb b/app/models/group.rb index f29e642ac91..70a4ceeffd8 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -18,6 +18,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 :milestones has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :shared_projects, through: :project_group_links, source: :project has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb index 86d38e5468b..65249bd7bfc 100644 --- a/app/models/group_milestone.rb +++ b/app/models/group_milestone.rb @@ -16,4 +16,8 @@ class GroupMilestone < GlobalMilestone def issues_finder_params { group_id: group.id } end + + def is_legacy_group_milestone? + true + end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 2fc6191e785..6ea774470af 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -849,7 +849,10 @@ class MergeRequest < ActiveRecord::Base # def all_commit_shas 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 compare_commits.to_a.reverse.map(&:id) else diff --git a/app/models/milestone.rb b/app/models/milestone.rb index c0ccbf8e27e..48d00764965 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -18,17 +18,32 @@ class Milestone < ActiveRecord::Base cache_markdown_field :description belongs_to :project + belongs_to :group + has_many :issues has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues has_many :merge_requests 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 :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 } - validates :project, presence: true + validate :uniqueness_of_title, if: :title_changed? + validate :milestone_type_check validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? } strip_attributes :title @@ -63,6 +78,14 @@ class Milestone < ActiveRecord::Base where(t[:title].matches(pattern).or(t[:description].matches(pattern))) end + + def filter_by_state(milestones, state) + case state + when 'closed' then milestones.closed + when 'all' then milestones + else milestones.active + end + end end def self.reference_prefix @@ -138,6 +161,8 @@ class Milestone < ActiveRecord::Base # Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1" # def to_reference(from_project = nil, format: :iid, full: false) + return if is_group_milestone? + format_reference = milestone_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" @@ -152,6 +177,10 @@ class Milestone < ActiveRecord::Base id end + def for_display + self + end + def can_be_closed? active? && issues.opened.count.zero? end @@ -164,8 +193,45 @@ class Milestone < ActiveRecord::Base write_attribute(:title, sanitize_title(value)) if value.present? 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 + # 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) raise ArgumentError, 'Unknown format' unless [:iid, :name].include?(format) diff --git a/app/serializers/label_entity.rb b/app/serializers/label_entity.rb index ad565654342..4452161051e 100644 --- a/app/serializers/label_entity.rb +++ b/app/serializers/label_entity.rb @@ -1,5 +1,6 @@ class LabelEntity < Grape::Entity - expose :id + expose :id, if: ->(label, _) { !label.is_a?(GlobalLabel) } + expose :title expose :color expose :description diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb index 68f6a8619e5..9eedb9e65a2 100644 --- a/app/services/boards/create_service.rb +++ b/app/services/boards/create_service.rb @@ -1,19 +1,22 @@ module Boards class CreateService < BaseService def execute - if project.boards.empty? - create_board! - else - project.boards.first - end + create_board! if can_create_board? end private + def can_create_board? + project.boards.size == 0 + end + def create_board! - board = project.boards.create - board.lists.create(list_type: :backlog) - board.lists.create(list_type: :closed) + board = project.boards.create(params) + + if board.persisted? + board.lists.create(list_type: :backlog) + board.lists.create(list_type: :closed) + end board end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 8dd0846f3bc..a03a7abfeb1 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -2,8 +2,11 @@ class IssuableBaseService < BaseService private def create_milestone_note(issuable) + milestone = issuable.milestone + return if milestone && milestone.is_group_milestone? + SystemNoteService.change_milestone( - issuable, issuable.project, current_user, issuable.milestone) + issuable, issuable.project, current_user, milestone) end def create_labels_note(issuable, old_labels) @@ -89,10 +92,12 @@ class IssuableBaseService < BaseService milestone_id = params[:milestone_id] return unless milestone_id - if milestone_id == IssuableFinder::NONE || - project.milestones.find_by(id: milestone_id).nil? - params[:milestone_id] = '' - end + params[:milestone_id] = '' if milestone_id == IssuableFinder::NONE + + milestone = + Milestone.for_projects_and_groups([project.id], [project.group&.id]).find_by_id(milestone_id) + + params[:milestone_id] = '' unless milestone end def filter_labels diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 711f4035c55..29def25719d 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -61,8 +61,18 @@ module Issues end def cloneable_milestone_id - @new_project.milestones - .find_by(title: @old_issue.milestone.try(:title)).try(:id) + title = @old_issue.milestone&.title + 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 def rewrite_notes diff --git a/app/services/milestones/base_service.rb b/app/services/milestones/base_service.rb index 176ab9f1ab5..4963601ea8b 100644 --- a/app/services/milestones/base_service.rb +++ b/app/services/milestones/base_service.rb @@ -1,4 +1,10 @@ module Milestones 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 + end end end diff --git a/app/services/milestones/close_service.rb b/app/services/milestones/close_service.rb index 608fc49d766..776ec4b287b 100644 --- a/app/services/milestones/close_service.rb +++ b/app/services/milestones/close_service.rb @@ -1,7 +1,7 @@ module Milestones class CloseService < Milestones::BaseService def execute(milestone) - if milestone.close + if milestone.close && milestone.is_project_milestone? event_service.close_milestone(milestone, current_user) end diff --git a/app/services/milestones/create_service.rb b/app/services/milestones/create_service.rb index b8e08c9f1eb..aef3124c7e3 100644 --- a/app/services/milestones/create_service.rb +++ b/app/services/milestones/create_service.rb @@ -1,9 +1,9 @@ module Milestones class CreateService < Milestones::BaseService 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) end diff --git a/app/services/milestones/reopen_service.rb b/app/services/milestones/reopen_service.rb index 573f9ee5c21..5b8b682caaf 100644 --- a/app/services/milestones/reopen_service.rb +++ b/app/services/milestones/reopen_service.rb @@ -1,7 +1,7 @@ module Milestones class ReopenService < Milestones::BaseService def execute(milestone) - if milestone.activate + if milestone.activate && milestone.is_project_milestone? event_service.reopen_milestone(milestone, current_user) end diff --git a/app/services/milestones/update_service.rb b/app/services/milestones/update_service.rb index ed64847f429..31b441ed476 100644 --- a/app/services/milestones/update_service.rb +++ b/app/services/milestones/update_service.rb @@ -5,9 +5,9 @@ module Milestones case state when 'activate' - Milestones::ReopenService.new(project, current_user, {}).execute(milestone) + Milestones::ReopenService.new(parent, current_user, {}).execute(milestone) when 'close' - Milestones::CloseService.new(project, current_user, {}).execute(milestone) + Milestones::CloseService.new(parent, current_user, {}).execute(milestone) end if params.present? diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index e4dfe87e614..6f82159e6c7 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -146,32 +146,6 @@ module QuickActions end end - desc do - "Change assignee#{'(s)' if issuable.allows_multiple_assignees?}" - end - explanation do |users| - users = issuable.allows_multiple_assignees? ? users : users.take(1) - "Change #{'assignee'.pluralize(users.size)} to #{users.map(&:to_reference).to_sentence}." - end - params do - issuable.allows_multiple_assignees? ? '@user1 @user2' : '@user' - end - condition do - issuable.persisted? && - current_user.can?(:"admin_#{issuable.to_ability_name}", project) - end - parse_params do |assignee_param| - extract_users(assignee_param) - end - command :reassign do |users| - @updates[:assignee_ids] = - if issuable.allows_multiple_assignees? - users.map(&:id) - else - [users.last.id] - end - end - desc 'Set milestone' explanation do |milestone| "Sets the milestone to #{milestone.to_reference}." if milestone diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml new file mode 100644 index 00000000000..7f450cd9a93 --- /dev/null +++ b/app/views/groups/milestones/_form.html.haml @@ -0,0 +1,27 @@ += 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" + diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml index 4c4e0a26728..bae8997e24c 100644 --- a/app/views/groups/milestones/_milestone.html.haml +++ b/app/views/groups/milestones/_milestone.html.haml @@ -1,5 +1,6 @@ + = 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), merge_requests_path: merge_requests_group_path(@group, milestone_title: milestone.title), milestone: milestone diff --git a/app/views/groups/milestones/edit.html.haml b/app/views/groups/milestones/edit.html.haml new file mode 100644 index 00000000000..5f6d7d209d0 --- /dev/null +++ b/app/views/groups/milestones/edit.html.haml @@ -0,0 +1,7 @@ +- page_title "Milestones" +- render "header_title" + +%h3.page-title + Edit Milestone + += render "form" diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index f91bee0b610..6ceb4092307 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -9,11 +9,6 @@ = link_to new_group_milestone_path(@group), class: "btn btn-new" do New milestone -.row-content-block - Only milestones from - %strong= @group.name - group are listed here. - .milestones %ul.content-list - if @milestones.blank? diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index 7c7573862d0..e24844661ee 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -4,40 +4,4 @@ %h3.page-title New Milestone -%p.light - 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 "form" diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index 33e68bc766e..54b1b7a734a 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -1,4 +1,4 @@ = render "header_title" = 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 diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 14deb46eee3..fb90bb4b472 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -57,16 +57,17 @@ %span 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 = 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 %span 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 %li.hidden diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index 7ed467c8841..1f18490594b 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -5,7 +5,7 @@ %strong #{@project.name} %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 = 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" } diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml index 03b33eb2da7..f6ca8d5a921 100644 --- a/app/views/projects/project_members/import.html.haml +++ b/app/views/projects/project_members/import.html.haml @@ -12,4 +12,4 @@ .form-actions = 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" diff --git a/app/views/projects/project_members/_index.html.haml b/app/views/projects/project_members/index.html.haml index 08e2d6177ab..25153fd0b6f 100644 --- a/app/views/projects/project_members/_index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,3 +1,5 @@ +- page_title "Members" + .row.prepend-top-default .col-lg-12 %h4 diff --git a/app/views/projects/settings/_head.html.haml b/app/views/projects/settings/_head.html.haml index b5773acb5a4..15ba09b10ba 100644 --- a/app/views/projects/settings/_head.html.haml +++ b/app/views/projects/settings/_head.html.haml @@ -9,10 +9,6 @@ = link_to edit_project_path(@project), title: 'General' do %span General - = nav_link(controller: :members) do - = link_to project_settings_members_path(@project), title: 'Members' do - %span - Members - if can_edit = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do = link_to project_settings_integrations_path(@project), title: 'Integrations' do diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml index a7c67ac9980..3739f4c221d 100644 --- a/app/views/shared/milestones/_issuable.html.haml +++ b/app/views/shared/milestones/_issuable.html.haml @@ -1,14 +1,13 @@ -# @project is present when viewing Project's milestone - project = @project || issuable.project - namespace = @project_namespace || project.namespace.becomes(Namespace) +- labels = issuable.labels - assignees = issuable.assignees -- issuable_type = issuable.class.table_name - base_url_args = [namespace, project] -- issuable_type_args = base_url_args + [issuable_type] +- issuable_type_args = base_url_args + [issuable.class.table_name] - issuable_url_args = base_url_args + [issuable] -- can_update = can?(current_user, :"update_#{issuable.to_ability_name}", issuable) -%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row #{'is-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-id' => issuable.id, 'data-url' => polymorphic_path(issuable_url_args) } +%li.issuable-row %span - if show_project_name %strong #{project.name} · @@ -18,10 +17,10 @@ = confidential_icon(issuable) = link_to issuable.title, issuable_url_args, title: issuable.title .issuable-detail - = link_to [project.namespace.becomes(Namespace), project, issuable] do + = link_to [namespace, project, issuable] do %span.issuable-number= issuable.to_reference - - issuable.labels.each do |label| + - labels.each do |label| = link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do - render_colored_label(label) diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index ecc8b42979c..6f6a036b13f 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -1,10 +1,15 @@ - dashboard = local_assigns[:dashboard] -- custom_dom_id = dom_id(@project ? milestone : milestone.milestones.first) +- custom_dom_id = dom_id(milestone.try(:milestones) ? milestone.milestones.first : milestone) %li{ class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id } .row .col-sm-6 %strong= link_to truncate(milestone.title, length: 100), milestone_path + - if milestone.is_group_milestone? + %span - Group Milestone + - else + %span - Project Milestone + .col-sm-6 .pull-right.light #{milestone.percent_complete(current_user)}% complete .row @@ -13,26 +18,32 @@ · = link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path .col-sm-6= milestone_progress_bar(milestone) - - if milestone.is_a?(GlobalMilestone) + - if milestone.is_a?(GlobalMilestone) || milestone.is_group_milestone? .row .col-sm-6 - .expiration= render('shared/milestone_expired', milestone: milestone) - .projects - - milestone.milestones.each do |milestone| - = link_to milestone_path(milestone) do - %span.label.label-gray - = dashboard ? milestone.project.name_with_namespace : milestone.project.name + - if milestone.is_legacy_group_milestone? + .expiration= render('shared/milestone_expired', milestone: milestone) + .projects + - milestone.milestones.each do |milestone| + = link_to milestone_path(milestone) do + %span.label.label-gray + = dashboard ? milestone.project.name_with_namespace : milestone.project.name - if @group - .col-sm-6 + .col-sm-6.milestone-actions - if can?(current_user, :admin_milestones, @group) + - if milestone.is_group_milestone? + = link_to edit_group_milestone_path(@group, milestone.id), class: "btn btn-xs btn-grouped" do + Edit + \ - if milestone.closed? - = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen" + = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen" - else - = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-xs btn-close" + = link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-xs btn-grouped btn-close" - if @project .row - .col-sm-6= render('shared/milestone_expired', milestone: milestone) + .col-sm-6 + = render('shared/milestone_expired', milestone: milestone) .col-sm-6.milestone-actions - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? = link_to edit_project_milestone_path(milestone.project, milestone), class: "btn btn-xs btn-grouped" do diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index e2d1695b7c3..b95a4ea674d 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -1,8 +1,10 @@ +- issues_accessible = milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) + .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .fade-left= icon('angle-left') .fade-right= icon('angle-right') %ul.nav-links.scrolling-tabs.js-milestone-tabs - - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) + - if issues_accessible %li.active = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do Issues @@ -25,13 +27,14 @@ Labels %span.badge= milestone.labels.count +- issues = milestone.sorted_issues(current_user) - show_project_name = local_assigns.fetch(:show_project_name, false) - show_full_project_name = local_assigns.fetch(:show_full_project_name, false) .tab-content.milestone-content - - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) + - if issues_accessible .tab-pane.active#tab-issues{ data: { sort_endpoint: (sort_issues_project_milestone_path(@project, @milestone) if @project && current_user) } } - = render 'shared/milestones/issues_tab', issues: milestone.sorted_issues(current_user), show_project_name: show_project_name, show_full_project_name: show_full_project_name + = render 'shared/milestones/issues_tab', issues: issues, show_project_name: show_project_name, show_full_project_name: show_full_project_name .tab-pane#tab-merge-requests -# loaded async = render "shared/milestones/tab_loading" diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index 20a12613cfc..b93837e3087 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -22,39 +22,55 @@ - if group .pull-right - if can?(current_user, :admin_milestones, group) + - if milestone.is_group_milestone? + = link_to edit_group_milestone_path(group, milestone.iid), class: "btn btn btn-grouped" do + Edit - if milestone.active? - = link_to 'Close Milestone', group_milestone_path(group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close" + = link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-grouped btn-close" - else - = link_to 'Reopen Milestone', group_milestone_path(group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" + = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" .detail-page-description.milestone-detail %h2.title = markdown_field(milestone, :title) + - if @milestone.is_group_milestone? && @milestone.description.present? + %div + .description + .wiki + = markdown_field(@milestone, :description) - if milestone.complete?(current_user) && milestone.active? .alert.alert-success.prepend-top-default - close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.' %span All issues for this milestone are closed. #{close_msg} -.table-holder - %table.table - %thead - %tr - %th Project - %th Open issues - %th State - %th Due date - - milestone.milestones.each do |ms| - %tr - %td - - project_name = group ? ms.project.name : ms.project.name_with_namespace - = link_to project_name, project_milestone_path(ms.project, ms) - %td - = ms.issues_visible_to_user(current_user).opened.count - %td - - if ms.closed? - Closed - - else - Open - %td - = ms.expires_at +- if @milestone.is_legacy_group_milestone? || @milestone.is_dashboard_milestone? + .table-holder + %table.table + %thead + %tr + %th Project + %th Open issues + %th State + %th Due date + - milestone.milestones.each do |ms| + %tr + %td + - project_name = group ? ms.project.name : ms.project.name_with_namespace + = link_to project_name, project_milestone_path(ms.project, ms) + %td + = ms.issues_visible_to_user(current_user).opened.count + %td + - if ms.closed? + Closed + - else + Open + %td + = ms.expires_at +- elsif @milestone.is_group_milestone? + %br + View + = link_to 'Issues', issues_group_path(@group, milestone_title: milestone.title) + or + = link_to 'Merge Requests', merge_requests_group_path(@group, milestone_title: milestone.title) + in this milestone |