summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/close_reopen_report_toggle.js97
-rw-r--r--app/assets/javascripts/comment_type_toggle.js5
-rw-r--r--app/assets/javascripts/diff.js7
-rw-r--r--app/assets/javascripts/dispatcher.js32
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js1
-rw-r--r--app/assets/javascripts/helpers/issuables_helper.js27
-rw-r--r--app/assets/javascripts/issuable_form.js2
-rw-r--r--app/assets/javascripts/issue.js55
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue3
-rw-r--r--app/assets/javascripts/main.js13
-rw-r--r--app/assets/javascripts/merge_request.js14
-rw-r--r--app/assets/javascripts/notes.js4
-rw-r--r--app/assets/javascripts/peek.js16
-rw-r--r--app/assets/javascripts/performance_bar.js62
-rw-r--r--app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js3
-rw-r--r--app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js71
-rw-r--r--app/assets/javascripts/project_new.js4
-rw-r--r--app/assets/javascripts/shortcuts.js2
-rw-r--r--app/assets/javascripts/signin_tabs_memoizer.js4
-rw-r--r--app/assets/javascripts/single_file_diff.js37
-rw-r--r--app/assets/javascripts/snippets_list.js10
-rw-r--r--app/assets/javascripts/star.js8
-rw-r--r--app/assets/javascripts/subscription_select.js8
-rw-r--r--app/assets/javascripts/task_list.js5
-rw-r--r--app/assets/javascripts/todos.js5
-rw-r--r--app/assets/javascripts/tree.js14
-rw-r--r--app/assets/javascripts/usage_ping.js5
-rw-r--r--app/assets/javascripts/user.js3
-rw-r--r--app/assets/javascripts/user_tabs.js5
-rw-r--r--app/assets/javascripts/username_validator.js4
-rw-r--r--app/assets/javascripts/version_check_image.js3
-rw-r--r--app/assets/javascripts/visibility_select.js5
-rw-r--r--app/assets/javascripts/wikis.js6
-rw-r--r--app/assets/javascripts/zen_mode.js22
-rw-r--r--app/assets/stylesheets/framework/buttons.scss18
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss71
-rw-r--r--app/assets/stylesheets/framework/filters.scss17
-rw-r--r--app/assets/stylesheets/framework/modal.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss38
-rw-r--r--app/assets/stylesheets/new_nav.scss77
-rw-r--r--app/assets/stylesheets/new_sidebar.scss118
-rw-r--r--app/assets/stylesheets/pages/issuable.scss25
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss10
-rw-r--r--app/assets/stylesheets/pages/note_form.scss51
-rw-r--r--app/assets/stylesheets/pages/pipeline_schedules.scss81
-rw-r--r--app/assets/stylesheets/performance_bar.scss103
-rw-r--r--app/controllers/admin/application_settings_controller.rb2
-rw-r--r--app/controllers/application_controller.rb17
-rw-r--r--app/controllers/concerns/membership_actions.rb2
-rw-r--r--app/controllers/concerns/with_performance_bar.rb17
-rw-r--r--app/controllers/dashboard/labels_controller.rb9
-rw-r--r--app/controllers/groups/milestones_controller.rb77
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb24
-rw-r--r--app/controllers/groups/variables_controller.rb64
-rw-r--r--app/controllers/projects/group_links_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb3
-rw-r--r--app/controllers/projects/milestones_controller.rb29
-rw-r--r--app/controllers/projects/pipeline_schedules_controller.rb19
-rw-r--r--app/controllers/projects/project_members_controller.rb23
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb5
-rw-r--r--app/controllers/projects/settings/members_controller.rb27
-rw-r--r--app/controllers/projects/variables_controller.rb39
-rw-r--r--app/finders/concerns/created_at_filter.rb8
-rw-r--r--app/finders/issuable_finder.rb33
-rw-r--r--app/finders/milestones_finder.rb59
-rw-r--r--app/finders/users_finder.rb3
-rw-r--r--app/helpers/gitlab_routing_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb57
-rw-r--r--app/helpers/milestones_helper.rb14
-rw-r--r--app/helpers/nav_helper.rb1
-rw-r--r--app/helpers/performance_bar_helper.rb7
-rw-r--r--app/helpers/projects_helper.rb18
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/models/application_setting.rb43
-rw-r--r--app/models/blob_viewer/readme.rb6
-rw-r--r--app/models/ci/build.rb2
-rw-r--r--app/models/ci/group_variable.rb13
-rw-r--r--app/models/ci/pipeline_schedule.rb8
-rw-r--r--app/models/ci/pipeline_schedule_variable.rb8
-rw-r--r--app/models/ci/variable.rb1
-rw-r--r--app/models/concerns/created_at_filterable.rb12
-rw-r--r--app/models/concerns/each_batch.rb81
-rw-r--r--app/models/concerns/internal_id.rb3
-rw-r--r--app/models/concerns/issuable.rb1
-rw-r--r--app/models/concerns/milestoneish.rb16
-rw-r--r--app/models/concerns/sha_attribute.rb2
-rw-r--r--app/models/dashboard_milestone.rb4
-rw-r--r--app/models/global_label.rb2
-rw-r--r--app/models/global_milestone.rb47
-rw-r--r--app/models/group.rb10
-rw-r--r--app/models/group_milestone.rb4
-rw-r--r--app/models/issue.rb3
-rw-r--r--app/models/merge_request.rb6
-rw-r--r--app/models/milestone.rb72
-rw-r--r--app/models/project_services/gitlab_issue_tracker_service.rb2
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/project_wiki.rb4
-rw-r--r--app/models/user.rb1
-rw-r--r--app/policies/ci/pipeline_schedule_policy.rb10
-rw-r--r--app/policies/group_policy.rb2
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/presenters/ci/group_variable_presenter.rb25
-rw-r--r--app/presenters/ci/variable_presenter.rb25
-rw-r--r--app/serializers/label_entity.rb3
-rw-r--r--app/services/boards/create_service.rb19
-rw-r--r--app/services/chat_names/authorize_user_service.rb2
-rw-r--r--app/services/issuable_base_service.rb15
-rw-r--r--app/services/issues/move_service.rb14
-rw-r--r--app/services/merge_requests/refresh_service.rb5
-rw-r--r--app/services/milestones/base_service.rb6
-rw-r--r--app/services/milestones/close_service.rb2
-rw-r--r--app/services/milestones/create_service.rb4
-rw-r--r--app/services/milestones/reopen_service.rb2
-rw-r--r--app/services/milestones/update_service.rb4
-rw-r--r--app/services/quick_actions/interpret_service.rb26
-rw-r--r--app/validators/variable_duplicates_validator.rb13
-rw-r--r--app/views/admin/application_settings/_form.html.haml16
-rw-r--r--app/views/ci/variables/_content.html.haml (renamed from app/views/projects/variables/_content.html.haml)0
-rw-r--r--app/views/ci/variables/_form.html.haml (renamed from app/views/projects/variables/_form.html.haml)6
-rw-r--r--app/views/ci/variables/_index.html.haml (renamed from app/views/projects/variables/_index.html.haml)10
-rw-r--r--app/views/ci/variables/_show.html.haml9
-rw-r--r--app/views/ci/variables/_table.html.haml (renamed from app/views/projects/variables/_table.html.haml)6
-rw-r--r--app/views/groups/_settings_head.html.haml5
-rw-r--r--app/views/groups/milestones/_form.html.haml27
-rw-r--r--app/views/groups/milestones/_milestone.html.haml3
-rw-r--r--app/views/groups/milestones/edit.html.haml7
-rw-r--r--app/views/groups/milestones/index.html.haml5
-rw-r--r--app/views/groups/milestones/new.html.haml38
-rw-r--r--app/views/groups/milestones/show.html.haml2
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml4
-rw-r--r--app/views/groups/variables/show.html.haml1
-rw-r--r--app/views/help/_shortcuts.html.haml9
-rw-r--r--app/views/layouts/_head.html.haml4
-rw-r--r--app/views/layouts/_page.html.haml6
-rw-r--r--app/views/layouts/application.html.haml3
-rw-r--r--app/views/layouts/nav/_new_admin_sidebar.html.haml4
-rw-r--r--app/views/layouts/nav/_new_group_sidebar.html.haml2
-rw-r--r--app/views/layouts/nav/_new_project_sidebar.html.haml2
-rw-r--r--app/views/layouts/nav/_project.html.haml11
-rw-r--r--app/views/peek/views/_rblineprof.html.haml7
-rw-r--r--app/views/peek/views/_sql.html.haml8
-rw-r--r--app/views/projects/blob/viewers/_readme.html.haml2
-rw-r--r--app/views/projects/empty.html.haml3
-rw-r--r--app/views/projects/issues/show.html.haml23
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml33
-rw-r--r--app/views/projects/pipeline_schedules/_form.html.haml8
-rw-r--r--app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml2
-rw-r--r--app/views/projects/pipeline_schedules/_variable_row.html.haml17
-rw-r--r--app/views/projects/project_members/_group_members.html.haml18
-rw-r--r--app/views/projects/project_members/_shared_group_members.html.haml24
-rw-r--r--app/views/projects/project_members/_team.html.haml2
-rw-r--r--app/views/projects/project_members/import.html.haml2
-rw-r--r--app/views/projects/project_members/index.html.haml (renamed from app/views/projects/project_members/_index.html.haml)2
-rw-r--r--app/views/projects/settings/_head.html.haml4
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/projects/show.html.haml3
-rw-r--r--app/views/projects/variables/show.html.haml10
-rw-r--r--app/views/shared/issuable/_close_reopen_button.html.haml14
-rw-r--r--app/views/shared/issuable/_close_reopen_report_toggle.html.haml49
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml2
-rw-r--r--app/views/shared/milestones/_issuable.html.haml11
-rw-r--r--app/views/shared/milestones/_milestone.html.haml35
-rw-r--r--app/views/shared/milestones/_tabs.html.haml9
-rw-r--r--app/views/shared/milestones/_top.html.haml64
-rw-r--r--app/views/shared/notes/_comment_button.html.haml10
165 files changed, 2077 insertions, 741 deletions
diff --git a/app/assets/javascripts/close_reopen_report_toggle.js b/app/assets/javascripts/close_reopen_report_toggle.js
new file mode 100644
index 00000000000..882d20671cc
--- /dev/null
+++ b/app/assets/javascripts/close_reopen_report_toggle.js
@@ -0,0 +1,97 @@
+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;
diff --git a/app/assets/javascripts/comment_type_toggle.js b/app/assets/javascripts/comment_type_toggle.js
index df0ba86198c..c74184949df 100644
--- a/app/assets/javascripts/comment_type_toggle.js
+++ b/app/assets/javascripts/comment_type_toggle.js
@@ -1,5 +1,8 @@
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 {
constructor(opts = {}) {
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 e924fde60bf..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,9 +49,19 @@ 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';
+import PerformanceBar from './performance_bar';
(function() {
var Dispatcher;
@@ -127,7 +132,7 @@ import OAuthRememberMe from './oauth_remember_me';
break;
case 'sessions:new':
new UsernameValidator();
- new ActiveTabMemoizer();
+ new SigninTabsMemoizer();
new OAuthRememberMe({ container: $(".omniauth-container") }).bindEvents();
break;
case 'projects:boards:show':
@@ -163,7 +168,7 @@ import OAuthRememberMe from './oauth_remember_me';
new UsersSelect();
break;
case 'dashboard:todos:index':
- new gl.Todos();
+ new Todos();
break;
case 'dashboard:projects:index':
case 'dashboard:projects:starred':
@@ -317,7 +322,7 @@ import OAuthRememberMe from './oauth_remember_me';
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();
@@ -379,7 +384,7 @@ import OAuthRememberMe from './oauth_remember_me';
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();
@@ -395,6 +400,7 @@ import OAuthRememberMe from './oauth_remember_me';
initSettingsPanels();
break;
case 'projects:settings:ci_cd:show':
+ case 'groups:settings:ci_cd:show':
new gl.ProjectVariables();
break;
case 'ci:lints:create':
@@ -431,7 +437,7 @@ import OAuthRememberMe from './oauth_remember_me';
new Admin();
switch (path[1]) {
case 'cohorts':
- new gl.UsagePing();
+ new UsagePing();
break;
case 'groups':
new UsersSelect();
@@ -483,7 +489,7 @@ import OAuthRememberMe from './oauth_remember_me';
new NotificationsDropdown();
break;
case 'wikis':
- new gl.Wikis();
+ new Wikis();
shortcut_handler = new ShortcutsWiki();
new ZenMode();
new gl.GLForm($('.wiki-form'), true);
@@ -515,6 +521,10 @@ import OAuthRememberMe from './oauth_remember_me';
if (!shortcut_handler) {
new Shortcuts();
}
+
+ if (document.querySelector('#peek')) {
+ new PerformanceBar({ container: '#peek' });
+ }
};
Dispatcher.prototype.initSearch = function() {
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 2c56b718212..6cb9cfe1382 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -30,6 +30,7 @@ class GfmAutoComplete {
this.input.each((i, input) => {
const $input = $(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
// Needed for quick actions with suffixes (ex: /label ~)
$input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
diff --git a/app/assets/javascripts/helpers/issuables_helper.js b/app/assets/javascripts/helpers/issuables_helper.js
new file mode 100644
index 00000000000..52d0f7e43fc
--- /dev/null
+++ b/app/assets/javascripts/helpers/issuables_helper.js
@@ -0,0 +1,27 @@
+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;
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..2bee4fb045a 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -4,13 +4,14 @@
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';
+import IssuablesHelper from './helpers/issuables_helper';
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',
@@ -28,6 +29,11 @@ class Issue {
Issue.initMergeRequests();
Issue.initRelatedBranches();
+ this.closeButtons = $('a.btn-close');
+ this.reopenButtons = $('a.btn-reopen');
+
+ this.initCloseReopenReport();
+
if (Issue.createMrDropdownWrap) {
this.createMergeRequestDropdown = new CreateMergeRequestDropdown(Issue.createMrDropdownWrap);
}
@@ -35,13 +41,8 @@ class Issue {
initIssueBtnEventListeners() {
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;
e.preventDefault();
e.stopImmediatePropagation();
@@ -50,7 +51,9 @@ class Issue {
if (shouldSubmit) {
Issue.submitNoteForm($button.closest('form'));
}
- $button.prop('disabled', true);
+
+ this.disableCloseReopenButton($button);
+
url = $button.attr('href');
return $.ajax({
type: 'PUT',
@@ -58,15 +61,19 @@ class Issue {
})
.fail(() => new Flash(issueFailMessage))
.done((data) => {
+ const isClosedBadge = $('div.status-box-closed');
+ const isOpenBadge = $('div.status-box-open');
+ const projectIssuesCounter = $('.issue_counter');
+
if ('id' in data) {
$(document).trigger('issuable:change');
const isClosed = $button.hasClass('btn-close');
- closeButtons.toggleClass('hidden', isClosed);
- reopenButtons.toggleClass('hidden', !isClosed);
isClosedBadge.toggleClass('hidden', !isClosed);
isOpenBadge.toggleClass('hidden', isClosed);
+ this.toggleCloseReopenButton(isClosed);
+
let numProjectIssues = Number(projectIssuesCounter.text().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues));
@@ -83,12 +90,34 @@ class Issue {
} else {
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) {
var noteText;
noteText = form.find("textarea.js-note-text").val();
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..0db2abe507d 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -2,8 +2,9 @@
/* global MergeRequestTabs */
import 'vendor/jquery.waitforimages';
-import './task_list';
+import TaskList from './task_list';
import './merge_request_tabs';
+import IssuablesHelper from './helpers/issuables_helper';
(function() {
this.MergeRequest = (function() {
@@ -21,11 +22,14 @@ import './merge_request_tabs';
return _this.showAllCommits();
};
})(this));
+
this.initTabs();
this.initMRBtnListeners();
this.initCommitMessageListeners();
+ this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport();
+
if ($("a.btn-close").length) {
- this.taskList = new gl.TaskList({
+ this.taskList = new TaskList({
dataType: 'merge_request',
fieldName: 'description',
selector: '.detail-page-description',
@@ -64,11 +68,15 @@ import './merge_request_tabs';
if (shouldSubmit && $this.data('submitted')) {
return;
}
+
+ if (this.closeReopenReportToggle) this.closeReopenReportToggle.setDisable();
+
if (shouldSubmit) {
if ($this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')) {
e.preventDefault();
e.stopImmediatePropagation();
- return _this.submitNoteForm($this.closest('form'), $this);
+
+ _this.submitNoteForm($this.closest('form'), $this);
}
}
});
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/peek.js b/app/assets/javascripts/peek.js
deleted file mode 100644
index de1a99fa3bd..00000000000
--- a/app/assets/javascripts/peek.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import 'vendor/peek';
-import 'vendor/peek.performance_bar';
-
-$(document).on('click', '#peek-show-queries', (e) => {
- e.preventDefault();
- $('.peek-rblineprof-modal').hide();
- const $modal = $('#modal-peek-pg-queries');
- if ($modal.length) {
- $modal.modal('toggle');
- }
-});
-
-$(document).on('click', '.js-lineprof-file', (e) => {
- e.preventDefault();
- $(e.target).parents('.peek-rblineprof-file').find('.data').toggle();
-});
diff --git a/app/assets/javascripts/performance_bar.js b/app/assets/javascripts/performance_bar.js
new file mode 100644
index 00000000000..9bbdf7f513c
--- /dev/null
+++ b/app/assets/javascripts/performance_bar.js
@@ -0,0 +1,62 @@
+import 'vendor/peek';
+import 'vendor/peek.performance_bar';
+
+export default class PerformanceBar {
+ constructor(opts) {
+ if (!PerformanceBar.singleton) {
+ this.init(opts);
+ PerformanceBar.singleton = this;
+ }
+ return PerformanceBar.singleton;
+ }
+
+ init(opts) {
+ const $container = $(opts.container);
+ this.$sqlProfileLink = $container.find('.js-toggle-modal-peek-sql');
+ this.$sqlProfileModal = $container.find('#modal-peek-pg-queries');
+ this.$lineProfileLink = $container.find('.js-toggle-modal-peek-line-profile');
+ this.$lineProfileModal = $('#modal-peek-line-profile');
+ this.initEventListeners();
+ this.showModalOnLoad();
+ }
+
+ initEventListeners() {
+ this.$sqlProfileLink.on('click', () => this.handleSQLProfileLink());
+ this.$lineProfileLink.on('click', e => this.handleLineProfileLink(e));
+ $(document).on('click', '.js-lineprof-file', PerformanceBar.toggleLineProfileFile);
+ }
+
+ showModalOnLoad() {
+ // When a lineprofiler query-string param is present, we show the line
+ // profiler modal upon page load
+ if (/lineprofiler/.test(window.location.search)) {
+ PerformanceBar.toggleModal(this.$lineProfileModal);
+ }
+ }
+
+ handleSQLProfileLink() {
+ PerformanceBar.toggleModal(this.$sqlProfileModal);
+ }
+
+ handleLineProfileLink(e) {
+ const lineProfilerParameter = gl.utils.getParameterValues('lineprofiler');
+ const lineProfilerParameterRegex = new RegExp(`lineprofiler=${lineProfilerParameter[0]}`);
+ const shouldToggleModal = lineProfilerParameter.length > 0 &&
+ lineProfilerParameterRegex.test(e.currentTarget.href);
+
+ if (shouldToggleModal) {
+ e.preventDefault();
+ PerformanceBar.toggleModal(this.$lineProfileModal);
+ }
+ }
+
+ static toggleModal($modal) {
+ if ($modal.length) {
+ $modal.modal('toggle');
+ }
+ }
+
+ static toggleLineProfileFile(e) {
+ $(e.currentTarget).parents('.peek-rblineprof-file').find('.data').toggle();
+ }
+}
diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js
index b424e7f205d..50c725aa3d5 100644
--- a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js
+++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js
@@ -3,6 +3,7 @@ import Translate from '../vue_shared/translate';
import intervalPatternInput from './components/interval_pattern_input.vue';
import TimezoneDropdown from './components/timezone_dropdown';
import TargetBranchDropdown from './components/target_branch_dropdown';
+import { setupPipelineVariableList } from './setup_pipeline_variable_list';
Vue.use(Translate);
@@ -39,4 +40,6 @@ document.addEventListener('DOMContentLoaded', () => {
gl.timezoneDropdown = new TimezoneDropdown();
gl.targetBranchDropdown = new TargetBranchDropdown();
gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement);
+
+ setupPipelineVariableList($('.js-pipeline-variable-list'));
});
diff --git a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js
new file mode 100644
index 00000000000..644efd10509
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js
@@ -0,0 +1,71 @@
+function insertRow($row) {
+ const $rowClone = $row.clone();
+ $rowClone.removeAttr('data-is-persisted');
+ $rowClone.find('input, textarea').val('');
+ $row.after($rowClone);
+}
+
+function removeRow($row) {
+ const isPersisted = gl.utils.convertPermissionToBoolean($row.attr('data-is-persisted'));
+
+ if (isPersisted) {
+ $row.hide();
+ $row
+ .find('.js-destroy-input')
+ .val(1);
+ } else {
+ $row.remove();
+ }
+}
+
+function checkIfRowTouched($row) {
+ return $row.find('.js-user-input').toArray().some(el => $(el).val().length > 0);
+}
+
+function setupPipelineVariableList(parent = document) {
+ const $parent = $(parent);
+
+ $parent.on('click', '.js-row-remove-button', (e) => {
+ const $row = $(e.currentTarget).closest('.js-row');
+ removeRow($row);
+
+ e.preventDefault();
+ });
+
+ // Remove any empty rows except the last r
+ $parent.on('blur', '.js-user-input', (e) => {
+ const $row = $(e.currentTarget).closest('.js-row');
+
+ const isTouched = checkIfRowTouched($row);
+ if ($row.is(':not(:last-child)') && !isTouched) {
+ removeRow($row);
+ }
+ });
+
+ // Always make sure there is an empty last row
+ $parent.on('input', '.js-user-input', () => {
+ const $lastRow = $parent.find('.js-row').last();
+
+ const isTouched = checkIfRowTouched($lastRow);
+ if (isTouched) {
+ insertRow($lastRow);
+ }
+ });
+
+ // Clear out the empty last row so it
+ // doesn't get submitted and throw validation errors
+ $parent.closest('form').on('submit', () => {
+ const $lastRow = $parent.find('.js-row').last();
+
+ const isTouched = checkIfRowTouched($lastRow);
+ if (!isTouched) {
+ $lastRow.find('input, textarea').attr('name', '');
+ }
+ });
+}
+
+export {
+ setupPipelineVariableList,
+ insertRow,
+ removeRow,
+};
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/shortcuts.js b/app/assets/javascripts/shortcuts.js
index a4a7f3fa944..49d980212d6 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -62,7 +62,7 @@ import findAndFollowLink from './shortcuts_dashboard_navigation';
if (Cookies.get(performanceBarCookieName) === 'true') {
Cookies.remove(performanceBarCookieName, { path: '/' });
} else {
- Cookies.set(performanceBarCookieName, true, { path: '/' });
+ Cookies.set(performanceBarCookieName, 'true', { path: '/' });
}
gl.utils.refreshCurrentPage();
};
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/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 4369ae78bde..6eabdc63d9e 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -20,17 +20,29 @@
color: $text;
border-color: $border;
+ > .icon {
+ color: $text;
+ }
+
&:hover,
&:focus {
background-color: $hover-background;
border-color: $hover-border;
color: $hover-text;
+
+ > .icon {
+ color: $hover-text;
+ }
}
&:active {
background-color: $active-background;
border-color: $active-border;
color: $hover-text;
+
+ > .icon {
+ color: $hover-text;
+ }
}
}
@@ -163,7 +175,8 @@
@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);
}
@@ -181,7 +194,8 @@
float: right;
}
- &.btn-reopen {
+ &.btn-reopen,
+ .btn-reopen-color {
/* should be same as parent class for now */
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 4f54ca24940..dc4ed42544f 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -295,9 +295,74 @@
}
}
-.filtered-search-box-input-container .dropdown-menu,
-.filtered-search-box-input-container .dropdown-menu-nav,
-.comment-type-dropdown .dropdown-menu {
+.droplab-dropdown {
+ .description {
+ 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;
opacity: 1;
visibility: visible;
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 767cf5ffea5..f05348ee4e3 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -70,6 +70,13 @@
.input-token {
max-width: 200px;
+ padding: 0;
+
+ &:hover,
+ &:focus {
+ background-color: inherit;
+ color: inherit;
+ }
}
.input-token:only-child,
@@ -156,6 +163,16 @@
}
}
+.droplab-dropdown li.filtered-search-token {
+ padding: 0;
+
+ &:hover,
+ &:focus {
+ background-color: inherit;
+ color: inherit;
+ }
+}
+
.filtered-search-term {
.name {
background-color: inherit;
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 7098203321d..a28f54936be 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -21,3 +21,9 @@ body.modal-open {
width: 860px;
}
}
+
+@media (min-width: $screen-lg-min) {
+ .modal-full {
+ width: 98%;
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index da4d91511e0..3f032776d82 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -74,11 +74,17 @@ $red-700: #a62d19;
$red-800: #8b2615;
$red-900: #711e11;
-$purple-600: #6e49cb;
-$purple-650: #5c35ae;
-$purple-700: #4a2192;
-$purple-800: #2c0a5c;
-$purple-900: #380d75;
+$indigo-50: #f7f7ff;
+$indigo-100: #ebebfa;
+$indigo-200: #d1d1f0;
+$indigo-300: #a6a6de;
+$indigo-400: #7c7ccc;
+$indigo-500: #6666c4;
+$indigo-600: #5b5bbd;
+$indigo-700: #4b4ba3;
+$indigo-800: #393982;
+$indigo-900: #292961;
+$indigo-950: #1a1a40;
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
@@ -153,6 +159,7 @@ $code_line_height: 1.6;
* Padding
*/
$gl-padding: 16px;
+$gl-col-padding: 15px;
$gl-btn-padding: 10px;
$gl-input-padding: 10px;
$gl-vert-padding: 6px;
@@ -264,7 +271,7 @@ $diff-view-modes-border: #c1c1c1;
/*
* 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";
/*
@@ -443,6 +450,7 @@ $logs-p-color: #333;
/*
* Forms
*/
+$input-height: 34px;
$input-danger-bg: #f2dede;
$input-danger-border: $red-400;
$input-group-addon-bg: #f7f8fa;
@@ -575,6 +583,12 @@ $stage-hover-border: #d1e7fc;
$action-icon-color: #d6d6d6;
/*
+Pipeline Schedules
+*/
+$pipeline-variable-remove-button-width: calc(1em + #{2 * $gl-padding});
+
+
+/*
Filtered Search
*/
$filter-name-resting-color: #f8f8f8;
@@ -594,3 +608,15 @@ Convdev Index
$color-high-score: $green-400;
$color-average-score: $orange-400;
$color-low-score: $red-400;
+
+/*
+Performance Bar
+*/
+$perf-bar-text: #999;
+$perf-bar-production: #222;
+$perf-bar-staging: #291430;
+$perf-bar-development: #4c1210;
+$perf-bar-bucket-bg: #111;
+$perf-bar-bucket-color: #ccc;
+$perf-bar-bucket-box-shadow-from: rgba($white-light, .2);
+$perf-bar-bucket-box-shadow-to: rgba($black, .25);
diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss
index bfb7a0c7e25..73cb3a7cf4c 100644
--- a/app/assets/stylesheets/new_nav.scss
+++ b/app/assets/stylesheets/new_nav.scss
@@ -4,7 +4,7 @@
header.navbar-gitlab-new {
color: $white-light;
- background-color: $purple-900;
+ background: linear-gradient(to right, $indigo-900, $indigo-800);
border-bottom: 0;
.header-content {
@@ -24,11 +24,9 @@ header.navbar-gitlab-new {
> a {
display: flex;
align-items: center;
- padding-top: 3px;
padding-right: $gl-padding;
padding-left: $gl-padding;
margin-left: -$gl-padding;
- border-bottom: 3px solid transparent;
@media (min-width: $screen-sm-min) {
padding-right: $gl-padding;
@@ -45,9 +43,8 @@ header.navbar-gitlab-new {
&:hover,
&:focus {
- color: currentColor;
+ color: $tanuki-yellow;
text-decoration: none;
- border-bottom-color: $white-light;
}
}
}
@@ -71,7 +68,7 @@ header.navbar-gitlab-new {
.navbar-collapse {
padding-left: 0;
- color: $white-light;
+ color: $indigo-200;
box-shadow: 0;
@media (max-width: $screen-xs-max) {
@@ -101,7 +98,7 @@ header.navbar-gitlab-new {
font-size: 14px;
text-align: center;
color: currentColor;
- border-left: 1px solid lighten($purple-700, 10%);
+ border-left: 1px solid lighten($indigo-700, 10%);
&:hover,
&:focus,
@@ -120,6 +117,7 @@ header.navbar-gitlab-new {
li {
.badge {
box-shadow: none;
+ font-weight: 600;
}
}
}
@@ -133,12 +131,11 @@ header.navbar-gitlab-new {
> a {
background: none;
- opacity: .9;
- will-change: opacity;
+ will-change: color;
&.header-user-dropdown-toggle {
.header-user-avatar {
- border-color: $white-light;
+ border-color: $indigo-200;
}
}
@@ -165,29 +162,34 @@ header.navbar-gitlab-new {
.navbar-sub-nav {
display: flex;
margin-bottom: 0;
- color: $white-light;
+ color: $indigo-200;
> li {
- &.active > a,
- a:hover,
- a:focus {
- border-bottom-color: $white-light;
+ > a:hover,
+ > a:focus {
+ box-shadow: inset 0 -3px 0 rgba($indigo-200, .4);
text-decoration: none;
outline: 0;
- opacity: 1;
+ color: $white-light;
+ }
+
+ &.active > a {
+ box-shadow: inset 0 -3px 0 $indigo-500;
+ color: $white-light;
+ font-weight: 700;
}
> a {
display: block;
- padding: 16px 10px 13px;
+ padding: 16px 10px;
font-size: 13px;
color: currentColor;
- border-bottom: 3px solid transparent;
- opacity: .9;
- will-change: opacity;
+ box-shadow: inset 0 0 0 transparent;
+ will-change: box-shadow;
+ transition: box-shadow 0.15s;
@media (min-width: $screen-sm-min) {
- padding: 15px $gl-padding 12px;
+ padding: 15px $gl-padding;
font-size: 14px;
}
}
@@ -207,55 +209,60 @@ header.navbar-gitlab-new {
.search {
form {
- border-color: $purple-800;
+ border: 0;
+ background-color: rgba($indigo-200, .2);
+ transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s, background-color ease-in-out 0.15s;
&:hover {
- border-color: rgba($white-light, .6);
+ background-color: rgba($indigo-200, .3);
box-shadow: none;
}
}
&.search-active form {
- border-color: $white-light;
- }
-
- form,
- .search-input {
- background-color: $purple-700;
+ background-color: rgba($indigo-200, .3);
+ box-shadow: none;
}
.search-input {
color: $white-light;
+ background: none;
}
.search-input::placeholder {
- color: rgba($white-light, .6);
+ color: rgba($indigo-200, .8);
}
.location-badge {
font-size: 12px;
- color: rgba($white-light, .6);
- background-color: $purple-800;
+ color: $indigo-100;
+ background-color: rgba($indigo-200, .1);
transition: color 0.15s;
will-change: color;
+ margin: -4px 4px -4px -4px;
+ line-height: 25px;
+ padding: 4px 8px;
+ border-radius: 2px 0 0 2px;
+ border-right: 1px solid $indigo-800;
+ height: 34px;
}
.search-input-wrap {
.search-icon,
.clear-icon {
- color: rgba($white-light, .6);
+ color: rgba($indigo-200, .8);
}
}
&.search-active {
.location-badge {
color: $white-light;
- background-color: $purple-800;
+ background-color: rgba($indigo-200, .2);
}
.search-input-wrap {
.search-icon {
- color: rgba($white-light, .6);
+ color: rgba($indigo-200, .8);
}
.clear-icon {
diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss
index 17f23f7fce3..96459fe31cc 100644
--- a/app/assets/stylesheets/new_sidebar.scss
+++ b/app/assets/stylesheets/new_sidebar.scss
@@ -2,6 +2,15 @@
@import 'framework/tw_bootstrap_variables';
@import "bootstrap/variables";
+$active-background: rgba(0,0,0,.04);
+$active-border: $indigo-500;
+$active-color: $indigo-700;
+$active-hover-background: $active-background;
+$active-hover-color: $gl-text-color;
+$inactive-badge-background: rgba(0,0,0,.08);
+$hover-background: $indigo-700;
+$hover-color: $white-light;
+$inactive-color: $gl-text-color-secondary;
$new-sidebar-width: 220px;
.page-with-new-sidebar {
@@ -17,24 +26,45 @@ $new-sidebar-width: 220px;
}
.context-header {
- background-color: $gray-normal;
border-bottom: 1px solid $border-color;
font-weight: 600;
display: flex;
align-items: center;
- padding: 10px 14px;
+ padding: 10px 16px 10px 10px;
+ color: $gl-text-color;
.avatar-container {
flex: 0 0 40px;
}
&:hover {
- background-color: $border-color;
+ background-color: $hover-background;
+ color: $hover-color;
+ border-color: $hover-background;
+
+ .avatar-container {
+ border-color: transparent;
+ }
+
+ .settings-avatar {
+ background-color: $indigo-500;
+
+ i {
+ color: $hover-color;
+ }
+ }
+ }
+
+ .project-title,
+ .group-title {
+ overflow: hidden;
+ text-overflow: ellipsis;
}
}
.settings-avatar {
background-color: $white-light;
+ transition: background-color 100ms linear;
i {
font-size: 20px;
@@ -42,6 +72,7 @@ $new-sidebar-width: 220px;
color: $gl-text-color-secondary;
text-align: center;
align-self: center;
+ transition: color 100ms linear;
}
}
@@ -54,11 +85,15 @@ $new-sidebar-width: 220px;
bottom: 0;
left: 0;
overflow: auto;
- background-color: $gray-light;
- border-right: 1px solid $border-color;
+ background-color: $gray-normal;
+ box-shadow: inset -2px 0 0 $border-color;
+
+ a {
+ text-decoration: none;
+ }
ul {
- padding: 0;
+ padding-left: 0;
list-style: none;
}
@@ -67,13 +102,18 @@ $new-sidebar-width: 220px;
a {
display: block;
- padding: 12px 14px;
+ padding: 12px 16px;
+ color: $inactive-color;
}
}
- a {
- color: $gl-text-color;
- text-decoration: none;
+ li.active {
+ box-shadow: inset 4px 0 0 $active-border;
+
+ > a {
+ color: $active-color;
+ font-weight: 700;
+ }
}
@media (max-width: $screen-xs-max) {
@@ -83,22 +123,28 @@ $new-sidebar-width: 220px;
.sidebar-sub-level-items {
display: none;
+ padding-bottom: 8px;
> li {
a {
- padding: 12px 24px;
- color: $gl-text-color-light;
+ font-size: 12px;
+ padding: 8px 16px 8px 24px;
- &:hover {
- color: $gl-text-color;
- background-color: $border-color;
+ &:hover,
+ &:focus {
+ background: $active-hover-background;
+ color: $active-hover-color;
}
}
&.active {
- > a {
- color: $purple-650;
- font-weight: 600;
+ a {
+ &,
+ &:hover,
+ &:focus {
+ background: $active-background;
+ color: $active-color;
+ }
}
}
}
@@ -108,35 +154,31 @@ $new-sidebar-width: 220px;
> li {
.badge {
float: right;
- background-color: $border-color;
- color: $gl-text-color;
+ background-color: $inactive-badge-background;
+ color: $inactive-color;
}
&.active {
- > a {
- background-color: $purple-600;
- color: $white-light;
- font-weight: 600;
- }
+ background: $active-background;
.badge {
- background-color: $purple-700;
- color: $white-light;
+ color: $active-color;
+ font-weight: 600;
}
.sidebar-sub-level-items {
- background-color: $gray-normal;
- border-left: 6px solid $purple-600;
display: block;
}
}
- &:not(.active) > a:hover {
- background-color: $border-color;
+ > a:hover {
+ background-color: $hover-background;
+ color: $hover-color;
.badge {
- transition: background-color 100ms linear;
- background-color: $gray-normal;
+ transition: background-color 100ms linear, color 100ms linear;
+ background-color: $indigo-500;
+ color: $hover-color;
}
}
}
@@ -155,3 +197,13 @@ $new-sidebar-width: 220px;
// scss-lint:enable DuplicateProperty
}
}
+
+
+// Change color of all horizontal tabs to match the new indigo color
+.nav-links li.active a {
+ border-bottom-color: $active-border;
+
+ .badge {
+ font-weight: 600;
+ }
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 47f50083726..56a4b53ed61 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -799,3 +799,28 @@
}
}
}
+
+.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;
+ }
+}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 59e0624d94e..7adf17dddb8 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -731,11 +731,11 @@
.merge-request-tabs-holder {
top: $header-height;
- z-index: 100;
+ z-index: 200;
background-color: $white-light;
border-bottom: 1px solid $border-color;
- @media(min-width: $screen-sm-min) {
+ @media (min-width: $screen-sm-min) {
position: sticky;
position: -webkit-sticky;
}
@@ -770,6 +770,12 @@
max-width: $limited-layout-width;
margin-left: auto;
margin-right: auto;
+
+ .inner-page-scroll-tabs {
+ background-color: $white-light;
+ margin-left: -$gl-padding;
+ padding-left: $gl-padding;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 9877ed2cfd6..cdb1e65e4be 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -356,7 +356,6 @@
color: $white-light;
padding-right: 2px;
margin-top: 2px;
- pointer-events: none;
}
}
@@ -366,56 +365,6 @@
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) {
display: flex;
diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss
index 595eb40fec7..dc719a6ba94 100644
--- a/app/assets/stylesheets/pages/pipeline_schedules.scss
+++ b/app/assets/stylesheets/pages/pipeline_schedules.scss
@@ -74,3 +74,84 @@
margin-right: 3px;
}
}
+
+.pipeline-variable-list {
+ margin-left: 0;
+ margin-bottom: 0;
+ padding-left: 0;
+ list-style: none;
+ clear: both;
+}
+
+.pipeline-variable-row {
+ display: flex;
+ align-items: flex-end;
+
+ &:not(:last-child) {
+ margin-bottom: $gl-btn-padding;
+ }
+
+ @media (max-width: $screen-sm-max) {
+ padding-right: $gl-col-padding;
+ }
+
+ &:last-child {
+ & .pipeline-variable-row-remove-button {
+ display: none;
+ }
+
+ @media (max-width: $screen-sm-max) {
+ & .pipeline-variable-value-input {
+ margin-right: $pipeline-variable-remove-button-width;
+ }
+ }
+
+ @media (max-width: $screen-xs-max) {
+ .pipeline-variable-row-body {
+ margin-right: $pipeline-variable-remove-button-width;
+ }
+ }
+ }
+}
+
+.pipeline-variable-row-body {
+ display: flex;
+ width: calc(75% - #{$gl-col-padding});
+ padding-left: $gl-col-padding;
+
+ @media (max-width: $screen-sm-max) {
+ width: 100%;
+ }
+
+ @media (max-width: $screen-xs-max) {
+ display: block;
+ }
+}
+
+.pipeline-variable-key-input {
+ margin-right: $gl-btn-padding;
+
+ @media (max-width: $screen-xs-max) {
+ margin-bottom: $gl-btn-padding;
+ }
+}
+
+.pipeline-variable-row-remove-button {
+ flex-shrink: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: $pipeline-variable-remove-button-width;
+ height: $input-height;
+ padding: 0;
+ background: transparent;
+ border: 0;
+ color: $gl-text-color-secondary;
+ @include transition(color);
+
+ &:hover,
+ &:focus {
+ outline: none;
+ color: $gl-text-color;
+ }
+}
diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss
new file mode 100644
index 00000000000..2890b6b1e49
--- /dev/null
+++ b/app/assets/stylesheets/performance_bar.scss
@@ -0,0 +1,103 @@
+@import "framework/variables";
+@import "peek/views/performance_bar";
+@import "peek/views/rblineprof";
+
+#peek {
+ height: 35px;
+ background: $black;
+ line-height: 35px;
+ color: $perf-bar-text;
+
+ &.disabled {
+ display: none;
+ }
+
+ &.production {
+ background-color: $perf-bar-production;
+ }
+
+ &.staging {
+ background-color: $perf-bar-staging;
+ }
+
+ &.development {
+ background-color: $perf-bar-development;
+ }
+
+ .wrapper {
+ width: 1000px;
+ margin: 0 auto;
+ }
+
+ // UI Elements
+ .bucket {
+ background: $perf-bar-bucket-bg;
+ display: inline-block;
+ padding: 4px 6px;
+ font-family: Consolas, "Liberation Mono", Courier, monospace;
+ line-height: 1;
+ color: $perf-bar-bucket-color;
+ border-radius: 3px;
+ box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from, inset 0 1px 2px $perf-bar-bucket-box-shadow-to;
+
+ .hidden {
+ display: none;
+ }
+
+ &:hover .hidden {
+ display: inline;
+ }
+ }
+
+ strong {
+ color: $white-light;
+ }
+
+ table {
+ color: $black;
+
+ strong {
+ color: $black;
+ }
+ }
+
+ .view {
+ margin-right: 15px;
+ float: left;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+
+ .css-truncate {
+ &.css-truncate-target,
+ .css-truncate-target {
+ display: inline-block;
+ max-width: 125px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ vertical-align: top;
+ }
+
+ &.expandable:hover .css-truncate-target,
+ &.expandable:hover.css-truncate-target {
+ max-width: 10000px !important;
+ }
+ }
+}
+
+#modal-peek-pg-queries-content {
+ color: $black;
+}
+
+.peek-rblineprof-file {
+ pre.duration {
+ width: 280px;
+ }
+
+ .data {
+ overflow: visible;
+ }
+}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index f978ce478c7..1cc060e4de8 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -126,6 +126,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:metrics_port,
:metrics_sample_interval,
:metrics_timeout,
+ :performance_bar_allowed_group_id,
+ :performance_bar_enabled,
:recaptcha_enabled,
:recaptcha_private_key,
:recaptcha_site_key,
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index b4c0cd0487f..db7edbd619b 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -9,7 +9,7 @@ class ApplicationController < ActionController::Base
include SentryHelper
include WorkhorseHelper
include EnforcesTwoFactorAuthentication
- include Peek::Rblineprof::CustomControllerHelpers
+ include WithPerformanceBar
before_action :authenticate_user_from_private_token!
before_action :authenticate_user_from_rss_token!
@@ -68,21 +68,6 @@ class ApplicationController < ActionController::Base
end
end
- def peek_enabled?
- return false unless Gitlab::PerformanceBar.enabled?
- return false unless current_user
-
- if RequestStore.active?
- if RequestStore.store.key?(:peek_enabled)
- RequestStore.store[:peek_enabled]
- else
- RequestStore.store[:peek_enabled] = cookies[:perf_bar_enabled].present?
- end
- else
- cookies[:perf_bar_enabled].present?
- end
- end
-
protected
# This filter handles both private tokens and personal access tokens
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/concerns/with_performance_bar.rb b/app/controllers/concerns/with_performance_bar.rb
new file mode 100644
index 00000000000..ed253042701
--- /dev/null
+++ b/app/controllers/concerns/with_performance_bar.rb
@@ -0,0 +1,17 @@
+module WithPerformanceBar
+ extend ActiveSupport::Concern
+
+ included do
+ include Peek::Rblineprof::CustomControllerHelpers
+ end
+
+ def peek_enabled?
+ return false unless Gitlab::PerformanceBar.enabled?(current_user)
+
+ if RequestStore.active?
+ RequestStore.fetch(:peek_enabled) { cookies[:perf_bar_enabled].present? }
+ else
+ cookies[:perf_bar_enabled].present?
+ end
+ end
+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/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
new file mode 100644
index 00000000000..0142ad8278c
--- /dev/null
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -0,0 +1,24 @@
+module Groups
+ module Settings
+ class CiCdController < Groups::ApplicationController
+ before_action :authorize_admin_pipeline!
+
+ def show
+ define_secret_variables
+ end
+
+ private
+
+ def define_secret_variables
+ @variable = Ci::GroupVariable.new(group: group)
+ .present(current_user: current_user)
+ @variables = group.variables.order_key_asc
+ .map { |variable| variable.present(current_user: current_user) }
+ end
+
+ def authorize_admin_pipeline!
+ return render_404 unless can?(current_user, :admin_pipeline, group)
+ end
+ end
+ end
+end
diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb
new file mode 100644
index 00000000000..10038ff3ad9
--- /dev/null
+++ b/app/controllers/groups/variables_controller.rb
@@ -0,0 +1,64 @@
+module Groups
+ class VariablesController < Groups::ApplicationController
+ before_action :variable, only: [:show, :update, :destroy]
+ before_action :authorize_admin_build!
+
+ def index
+ redirect_to group_settings_ci_cd_path(group)
+ end
+
+ def show
+ end
+
+ def update
+ if variable.update(variable_params)
+ redirect_to group_variables_path(group),
+ notice: 'Variable was successfully updated.'
+ else
+ render "show"
+ end
+ end
+
+ def create
+ @variable = group.variables.create(variable_params)
+ .present(current_user: current_user)
+
+ if @variable.persisted?
+ redirect_to group_settings_ci_cd_path(group),
+ notice: 'Variable was successfully created.'
+ else
+ render "show"
+ end
+ end
+
+ def destroy
+ if variable.destroy
+ redirect_to group_settings_ci_cd_path(group),
+ status: 302,
+ notice: 'Variable was successfully removed.'
+ else
+ redirect_to group_settings_ci_cd_path(group),
+ status: 302,
+ notice: 'Failed to remove the variable.'
+ end
+ end
+
+ private
+
+ def variable_params
+ params.require(:variable).permit(*variable_params_attributes)
+ end
+
+ def variable_params_attributes
+ %i[key value protected]
+ end
+
+ def variable
+ @variable ||= group.variables.find(params[:id]).present(current_user: current_user)
+ end
+
+ def authorize_admin_build!
+ return render_404 unless can?(current_user, :admin_build, group)
+ end
+ 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/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index 5de0f828010..6602b204fcb 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -17,8 +17,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
end
def merge_request_params
- params.require(:merge_request)
- .permit(merge_request_params_attributes)
+ params.require(:merge_request).permit(merge_request_params_attributes)
end
def merge_request_params_attributes
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/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb
index 0d967a7e691..ec7c645df5a 100644
--- a/app/controllers/projects/pipeline_schedules_controller.rb
+++ b/app/controllers/projects/pipeline_schedules_controller.rb
@@ -1,11 +1,11 @@
class Projects::PipelineSchedulesController < Projects::ApplicationController
+ before_action :schedule, except: [:index, :new, :create]
+
before_action :authorize_read_pipeline_schedule!
before_action :authorize_create_pipeline_schedule!, only: [:new, :create]
- before_action :authorize_update_pipeline_schedule!, only: [:edit, :take_ownership, :update]
+ before_action :authorize_update_pipeline_schedule!, except: [:index, :new, :create]
before_action :authorize_admin_pipeline_schedule!, only: [:destroy]
- before_action :schedule, only: [:edit, :update, :destroy, :take_ownership]
-
def index
@scope = params[:scope]
@all_schedules = PipelineSchedulesFinder.new(@project).execute
@@ -53,7 +53,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
redirect_to pipeline_schedules_path(@project), status: 302
else
redirect_to pipeline_schedules_path(@project),
- status: 302,
+ status: :forbidden,
alert: _("Failed to remove the pipeline schedule")
end
end
@@ -66,6 +66,15 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
def schedule_params
params.require(:schedule)
- .permit(:description, :cron, :cron_timezone, :ref, :active)
+ .permit(:description, :cron, :cron_timezone, :ref, :active,
+ variables_attributes: [:id, :key, :value, :_destroy] )
+ end
+
+ def authorize_update_pipeline_schedule!
+ return access_denied! unless can?(current_user, :update_pipeline_schedule, schedule)
+ end
+
+ def authorize_admin_pipeline_schedule!
+ return access_denied! unless can?(current_user, :admin_pipeline_schedule, schedule)
end
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/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 24fe78bc1bd..ea7ceb3eaa5 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -21,7 +21,10 @@ module Projects
end
def define_secret_variables
- @variable = Ci::Variable.new
+ @variable = Ci::Variable.new(project: project)
+ .present(current_user: current_user)
+ @variables = project.variables.order_key_asc
+ .map { |variable| variable.present(current_user: current_user) }
end
def define_triggers_variables
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/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index 326d31ecec2..6a825137564 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -1,4 +1,5 @@
class Projects::VariablesController < Projects::ApplicationController
+ before_action :variable, only: [:show, :update, :destroy]
before_action :authorize_admin_build!
layout 'project_settings'
@@ -8,37 +9,39 @@ class Projects::VariablesController < Projects::ApplicationController
end
def show
- @variable = @project.variables.find(params[:id])
end
def update
- @variable = @project.variables.find(params[:id])
-
- if @variable.update_attributes(variable_params)
- redirect_to project_variables_path(project), notice: 'Variable was successfully updated.'
+ if variable.update(variable_params)
+ redirect_to project_variables_path(project),
+ notice: 'Variable was successfully updated.'
else
- render action: "show"
+ render "show"
end
end
def create
- @variable = @project.variables.new(variable_params)
+ @variable = project.variables.create(variable_params)
+ .present(current_user: current_user)
- if @variable.save
- flash[:notice] = 'Variables were successfully updated.'
- redirect_to project_settings_ci_cd_path(project)
+ if @variable.persisted?
+ redirect_to project_settings_ci_cd_path(project),
+ notice: 'Variable was successfully created.'
else
render "show"
end
end
def destroy
- @key = @project.variables.find(params[:id])
- @key.destroy
-
- redirect_to project_settings_ci_cd_path(project),
- status: 302,
- notice: 'Variable was successfully removed.'
+ if variable.destroy
+ redirect_to project_settings_ci_cd_path(project),
+ status: 302,
+ notice: 'Variable was successfully removed.'
+ else
+ redirect_to project_settings_ci_cd_path(project),
+ status: 302,
+ notice: 'Failed to remove the variable.'
+ end
end
private
@@ -50,4 +53,8 @@ class Projects::VariablesController < Projects::ApplicationController
def variable_params_attributes
%i[id key value protected _destroy]
end
+
+ def variable
+ @variable ||= project.variables.find(params[:id]).present(current_user: current_user)
+ end
end
diff --git a/app/finders/concerns/created_at_filter.rb b/app/finders/concerns/created_at_filter.rb
new file mode 100644
index 00000000000..ac9ac77732c
--- /dev/null
+++ b/app/finders/concerns/created_at_filter.rb
@@ -0,0 +1,8 @@
+module CreatedAtFilter
+ def by_created_at(items)
+ items = items.created_before(params[:created_before]) if params[:created_before].present?
+ items = items.created_after(params[:created_after]) if params[:created_after].present?
+
+ items
+ end
+end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 7bc2117f61e..2e5a6493134 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -19,6 +19,8 @@
# iids: integer[]
#
class IssuableFinder
+ include CreatedAtFilter
+
NONE = '0'.freeze
IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze
@@ -32,6 +34,7 @@ class IssuableFinder
def execute
items = init_collection
items = by_scope(items)
+ items = by_created_at(items)
items = by_state(items)
items = by_group(items)
items = by_search(items)
@@ -42,7 +45,6 @@ class IssuableFinder
items = by_iids(items)
items = by_milestone(items)
items = by_label(items)
- items = by_created_at(items)
# Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far
items = by_project(items)
@@ -147,9 +149,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 +341,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
@@ -408,18 +413,6 @@ class IssuableFinder
params[:non_archived].present? ? items.non_archived : items
end
- def by_created_at(items)
- if params[:created_after].present?
- items = items.where(items.klass.arel_table[:created_at].gteq(params[:created_after]))
- end
-
- if params[:created_before].present?
- items = items.where(items.klass.arel_table[:created_at].lteq(params[:created_before]))
- end
-
- items
- end
-
def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
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/finders/users_finder.rb b/app/finders/users_finder.rb
index 07deceb827b..33f7ae90598 100644
--- a/app/finders/users_finder.rb
+++ b/app/finders/users_finder.rb
@@ -14,6 +14,8 @@
# external: boolean
#
class UsersFinder
+ include CreatedAtFilter
+
attr_accessor :current_user, :params
def initialize(current_user, params = {})
@@ -29,6 +31,7 @@ class UsersFinder
users = by_active(users)
users = by_external_identity(users)
users = by_external(users)
+ users = by_created_at(users)
users
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/issuables_helper.rb b/app/helpers/issuables_helper.rb
index b5366519ed9..d0c518f81f7 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -245,6 +245,53 @@ module IssuablesHelper
@counts[cache_key][state]
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
def sidebar_gutter_collapsed?
@@ -270,8 +317,6 @@ module IssuablesHelper
issue_template_names
when MergeRequest
merge_request_template_names
- else
- raise 'Unknown issuable type!'
end
end
@@ -301,4 +346,12 @@ module IssuablesHelper
container: (is_collapsed ? 'body' : nil)
}
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
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/nav_helper.rb b/app/helpers/nav_helper.rb
index e589ed4e56d..b769462abc2 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -23,7 +23,6 @@ module NavHelper
def nav_header_class
class_name = ''
class_name << " with-horizontal-nav" if defined?(nav) && nav
- class_name << " with-peek" if peek_enabled?
class_name
end
diff --git a/app/helpers/performance_bar_helper.rb b/app/helpers/performance_bar_helper.rb
new file mode 100644
index 00000000000..d24efe37f5f
--- /dev/null
+++ b/app/helpers/performance_bar_helper.rb
@@ -0,0 +1,7 @@
+module PerformanceBarHelper
+ # This is a hack since using `alias_method :performance_bar_enabled?, :peek_enabled?`
+ # in WithPerformanceBar breaks tests (but works in the browser).
+ def performance_bar_enabled?
+ peek_enabled?
+ 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/application_setting.rb b/app/models/application_setting.rb
index b0d7f7ef5f5..14516091495 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -234,6 +234,7 @@ class ApplicationSetting < ActiveRecord::Base
koding_url: nil,
max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
+ performance_bar_allowed_group_id: nil,
plantuml_enabled: false,
plantuml_url: nil,
recaptcha_enabled: false,
@@ -336,6 +337,48 @@ class ApplicationSetting < ActiveRecord::Base
super(levels.map { |level| Gitlab::VisibilityLevel.level_value(level) })
end
+ def performance_bar_allowed_group_id=(group_full_path)
+ group_full_path = nil if group_full_path.blank?
+
+ if group_full_path.nil?
+ if group_full_path != performance_bar_allowed_group_id
+ super(group_full_path)
+ Gitlab::PerformanceBar.expire_allowed_user_ids_cache
+ end
+ return
+ end
+
+ group = Group.find_by_full_path(group_full_path)
+
+ if group
+ if group.id != performance_bar_allowed_group_id
+ super(group.id)
+ Gitlab::PerformanceBar.expire_allowed_user_ids_cache
+ end
+ else
+ super(nil)
+ Gitlab::PerformanceBar.expire_allowed_user_ids_cache
+ end
+ end
+
+ def performance_bar_allowed_group
+ Group.find_by_id(performance_bar_allowed_group_id)
+ end
+
+ # Return true if the Performance Bar is enabled for a given group
+ def performance_bar_enabled
+ performance_bar_allowed_group_id.present?
+ end
+
+ # - If `enable` is true, we early return since the actual attribute that holds
+ # the enabling/disabling is `performance_bar_allowed_group_id`
+ # - If `enable` is false, we set `performance_bar_allowed_group_id` to `nil`
+ def performance_bar_enabled=(enable)
+ return if enable
+
+ self.performance_bar_allowed_group_id = nil
+ end
+
# Choose one of the available repository storage options. Currently all have
# equal weighting.
def pick_repository_storage
diff --git a/app/models/blob_viewer/readme.rb b/app/models/blob_viewer/readme.rb
index 75c373a03bb..4604a9934a0 100644
--- a/app/models/blob_viewer/readme.rb
+++ b/app/models/blob_viewer/readme.rb
@@ -10,5 +10,11 @@ module BlobViewer
def visible_to?(current_user)
can?(current_user, :read_wiki, project)
end
+
+ def render_error
+ return if project.has_external_wiki? || (project.wiki_enabled? && project.wiki.has_home_page?)
+
+ :no_wiki
+ end
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index ba2ecbf82a2..432f3f242eb 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -200,8 +200,10 @@ module Ci
variables += project.deployment_variables if has_environment?
variables += yaml_variables
variables += user_variables
+ variables += project.group.secret_variables_for(ref, project).map(&:to_runner_variable) if project.group
variables += secret_variables(environment: environment)
variables += trigger_request.user_variables if trigger_request
+ variables += pipeline.pipeline_schedule.job_variables if pipeline.pipeline_schedule
variables += persisted_environment_variables if environment
variables
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
new file mode 100644
index 00000000000..f64bc245a67
--- /dev/null
+++ b/app/models/ci/group_variable.rb
@@ -0,0 +1,13 @@
+module Ci
+ class GroupVariable < ActiveRecord::Base
+ extend Ci::Model
+ include HasVariable
+ include Presentable
+
+ belongs_to :group
+
+ validates :key, uniqueness: { scope: :group_id }
+
+ scope :unprotected, -> { where(protected: false) }
+ end
+end
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index 45d8cd34359..e4ae1b35f66 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -9,17 +9,21 @@ module Ci
belongs_to :owner, class_name: 'User'
has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline'
has_many :pipelines
+ has_many :variables, class_name: 'Ci::PipelineScheduleVariable'
validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? }
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? }
validates :description, presence: true
+ validates :variables, variable_duplicates: true
before_save :set_next_run_at
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
+ accepts_nested_attributes_for :variables, allow_destroy: true
+
def owned_by?(current_user)
owner == current_user
end
@@ -56,5 +60,9 @@ module Ci
Gitlab::Ci::CronParser.new(worker_cron, worker_time_zone)
.next_time_from(next_run_at)
end
+
+ def job_variables
+ variables&.map(&:to_runner_variable) || []
+ end
end
end
diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb
new file mode 100644
index 00000000000..1ff177616e8
--- /dev/null
+++ b/app/models/ci/pipeline_schedule_variable.rb
@@ -0,0 +1,8 @@
+module Ci
+ class PipelineScheduleVariable < ActiveRecord::Base
+ extend Ci::Model
+ include HasVariable
+
+ belongs_to :pipeline_schedule
+ end
+end
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 0b8d0ff881a..cf0fe04ddaf 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -2,6 +2,7 @@ module Ci
class Variable < ActiveRecord::Base
extend Ci::Model
include HasVariable
+ include Presentable
belongs_to :project
diff --git a/app/models/concerns/created_at_filterable.rb b/app/models/concerns/created_at_filterable.rb
new file mode 100644
index 00000000000..e8a3e41203d
--- /dev/null
+++ b/app/models/concerns/created_at_filterable.rb
@@ -0,0 +1,12 @@
+module CreatedAtFilterable
+ extend ActiveSupport::Concern
+
+ included do
+ scope :created_before, ->(date) { where(scoped_table[:created_at].lteq(date)) }
+ scope :created_after, ->(date) { where(scoped_table[:created_at].gteq(date)) }
+
+ def self.scoped_table
+ arel_table.alias(table_name)
+ end
+ end
+end
diff --git a/app/models/concerns/each_batch.rb b/app/models/concerns/each_batch.rb
new file mode 100644
index 00000000000..6ddbb8da1a9
--- /dev/null
+++ b/app/models/concerns/each_batch.rb
@@ -0,0 +1,81 @@
+module EachBatch
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Iterates over the rows in a relation in batches, similar to Rails'
+ # `in_batches` but in a more efficient way.
+ #
+ # Unlike `in_batches` provided by Rails this method does not support a
+ # custom start/end range, nor does it provide support for the `load:`
+ # keyword argument.
+ #
+ # This method will yield an ActiveRecord::Relation to the supplied block, or
+ # return an Enumerator if no block is given.
+ #
+ # Example:
+ #
+ # User.each_batch do |relation|
+ # relation.update_all(updated_at: Time.now)
+ # end
+ #
+ # The supplied block is also passed an optional batch index:
+ #
+ # User.each_batch do |relation, index|
+ # puts index # => 1, 2, 3, ...
+ # end
+ #
+ # You can also specify an alternative column to use for ordering the rows:
+ #
+ # User.each_batch(column: :created_at) do |relation|
+ # ...
+ # end
+ #
+ # This will produce SQL queries along the lines of:
+ #
+ # User Load (0.7ms) SELECT "users"."id" FROM "users" WHERE ("users"."id" >= 41654) ORDER BY "users"."id" ASC LIMIT 1 OFFSET 1000
+ # (0.7ms) SELECT COUNT(*) FROM "users" WHERE ("users"."id" >= 41654) AND ("users"."id" < 42687)
+ #
+ # of - The number of rows to retrieve per batch.
+ # column - The column to use for ordering the batches.
+ def each_batch(of: 1000, column: primary_key)
+ unless column
+ raise ArgumentError,
+ 'the column: argument must be set to a column name to use for ordering rows'
+ end
+
+ start = except(:select)
+ .select(column)
+ .reorder(column => :asc)
+ .take
+
+ return unless start
+
+ start_id = start[column]
+ arel_table = self.arel_table
+
+ 1.step do |index|
+ stop = except(:select)
+ .select(column)
+ .where(arel_table[column].gteq(start_id))
+ .reorder(column => :asc)
+ .offset(of)
+ .limit(1)
+ .take
+
+ relation = where(arel_table[column].gteq(start_id))
+
+ if stop
+ stop_id = stop[column]
+ start_id = stop_id
+ relation = relation.where(arel_table[column].lt(stop_id))
+ end
+
+ # Any ORDER BYs are useless for this relation and can lead to less
+ # efficient UPDATE queries, hence we get rid of it.
+ yield relation.except(:order), index
+
+ break unless stop
+ end
+ end
+ end
+end
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/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb
index c28974a3cdf..67ecf470f7e 100644
--- a/app/models/concerns/sha_attribute.rb
+++ b/app/models/concerns/sha_attribute.rb
@@ -3,6 +3,8 @@ module ShaAttribute
module ClassMethods
def sha_attribute(name)
+ return unless table_exists?
+
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,
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 b93fce6100d..70a4ceeffd8 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -18,10 +18,12 @@ 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
has_many :labels, class_name: 'GroupLabel'
+ has_many :variables, class_name: 'Ci::GroupVariable'
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :visibility_level_allowed_by_projects
@@ -248,6 +250,14 @@ class Group < Namespace
}
end
+ def secret_variables_for(ref, project)
+ list_of_ids = [self] + ancestors
+ variables = Ci::GroupVariable.where(group: list_of_ids)
+ variables = variables.unprotected unless project.protected_for?(ref)
+ variables = variables.group_by(&:group_id)
+ list_of_ids.reverse.map { |group| variables[group.id] }.compact.flatten
+ end
+
protected
def update_two_factor_requirement
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/issue.rb b/app/models/issue.rb
index 01f985823e1..400bb55d2f0 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -10,6 +10,7 @@ class Issue < ActiveRecord::Base
include FasterCacheKeys
include RelativePositioning
include IgnorableColumn
+ include CreatedAtFilterable
ignore_column :position
@@ -50,8 +51,6 @@ class Issue < ActiveRecord::Base
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 :created_after, -> (datetime) { where("created_at >= ?", datetime) }
-
scope :preload_associations, -> { preload(:labels, project: :namespace) }
after_save :expire_etag_cache
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 2fc6191e785..30caa3598d1 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -5,6 +5,7 @@ class MergeRequest < ActiveRecord::Base
include Referable
include Sortable
include IgnorableColumn
+ include CreatedAtFilterable
ignore_column :position
@@ -849,7 +850,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/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb
index 5e31f393bbe..420102875a5 100644
--- a/app/models/project_services/gitlab_issue_tracker_service.rb
+++ b/app/models/project_services/gitlab_issue_tracker_service.rb
@@ -1,5 +1,5 @@
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?
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 8af642b44aa..5498a2e17b2 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -1,5 +1,5 @@
class JiraService < IssueTrackerService
- include Gitlab::Routing.url_helpers
+ include Gitlab::Routing
validates :url, url: true, presence: true, if: :activated?
validates :api_url, url: true, allow_blank: true
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index beaadbbd1ab..dfca0031af8 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -63,6 +63,10 @@ class ProjectWiki
!!repository.exists?
end
+ def has_home_page?
+ !!find_page('home')
+ end
+
# Returns an Array of Gitlab WikiPage instances or an
# empty Array if this Wiki has no pages.
def pages
diff --git a/app/models/user.rb b/app/models/user.rb
index 4411a06d429..4b01c2f19f0 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -12,6 +12,7 @@ class User < ActiveRecord::Base
include TokenAuthenticatable
include IgnorableColumn
include FeatureGate
+ include CreatedAtFilterable
DEFAULT_NOTIFICATION_LEVEL = :participating
diff --git a/app/policies/ci/pipeline_schedule_policy.rb b/app/policies/ci/pipeline_schedule_policy.rb
index 1877e89bb23..6b7598e1821 100644
--- a/app/policies/ci/pipeline_schedule_policy.rb
+++ b/app/policies/ci/pipeline_schedule_policy.rb
@@ -1,4 +1,14 @@
module Ci
class PipelineSchedulePolicy < PipelinePolicy
+ alias_method :pipeline_schedule, :subject
+
+ condition(:owner_of_schedule) do
+ can?(:developer_access) && pipeline_schedule.owned_by?(@user)
+ end
+
+ rule { can?(:master_access) | owner_of_schedule }.policy do
+ enable :update_pipeline_schedule
+ enable :admin_pipeline_schedule
+ end
end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index dcb37416ca3..6defab75fce 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -31,6 +31,8 @@ class GroupPolicy < BasePolicy
rule { master }.policy do
enable :create_projects
enable :admin_milestones
+ enable :admin_pipeline
+ enable :admin_build
end
rule { owner }.policy do
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 7cbca63fab4..323131c0f7e 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -162,7 +162,6 @@ class ProjectPolicy < BasePolicy
enable :create_pipeline
enable :update_pipeline
enable :create_pipeline_schedule
- enable :update_pipeline_schedule
enable :create_merge_request
enable :create_wiki
enable :push_code
@@ -188,7 +187,6 @@ class ProjectPolicy < BasePolicy
enable :admin_build
enable :admin_container_image
enable :admin_pipeline
- enable :admin_pipeline_schedule
enable :admin_environment
enable :admin_deployment
enable :admin_pages
diff --git a/app/presenters/ci/group_variable_presenter.rb b/app/presenters/ci/group_variable_presenter.rb
new file mode 100644
index 00000000000..81fea106a5c
--- /dev/null
+++ b/app/presenters/ci/group_variable_presenter.rb
@@ -0,0 +1,25 @@
+module Ci
+ class GroupVariablePresenter < Gitlab::View::Presenter::Delegated
+ presents :variable
+
+ def placeholder
+ 'GROUP_VARIABLE'
+ end
+
+ def form_path
+ if variable.persisted?
+ group_variable_path(group, variable)
+ else
+ group_variables_path(group)
+ end
+ end
+
+ def edit_path
+ group_variable_path(group, variable)
+ end
+
+ def delete_path
+ group_variable_path(group, variable)
+ end
+ end
+end
diff --git a/app/presenters/ci/variable_presenter.rb b/app/presenters/ci/variable_presenter.rb
new file mode 100644
index 00000000000..5d7998393a6
--- /dev/null
+++ b/app/presenters/ci/variable_presenter.rb
@@ -0,0 +1,25 @@
+module Ci
+ class VariablePresenter < Gitlab::View::Presenter::Delegated
+ presents :variable
+
+ def placeholder
+ 'PROJECT_VARIABLE'
+ end
+
+ def form_path
+ if variable.persisted?
+ project_variable_path(project, variable)
+ else
+ project_variables_path(project)
+ end
+ end
+
+ def edit_path
+ project_variable_path(project, variable)
+ end
+
+ def delete_path
+ project_variable_path(project, variable)
+ end
+ end
+end
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/chat_names/authorize_user_service.rb b/app/services/chat_names/authorize_user_service.rb
index 321bf3a9205..7256466c9e8 100644
--- a/app/services/chat_names/authorize_user_service.rb
+++ b/app/services/chat_names/authorize_user_service.rb
@@ -1,6 +1,6 @@
module ChatNames
class AuthorizeUserService
- include Gitlab::Routing.url_helpers
+ include Gitlab::Routing
def initialize(service, params)
@service = service
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/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index c5f959a1874..bc4a13cf4bc 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -35,11 +35,12 @@ module MergeRequests
# target branch manually
def close_merge_requests
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 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
filter_merge_requests(merge_requests).each do |merge_request|
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/validators/variable_duplicates_validator.rb b/app/validators/variable_duplicates_validator.rb
new file mode 100644
index 00000000000..8a9d8892e9b
--- /dev/null
+++ b/app/validators/variable_duplicates_validator.rb
@@ -0,0 +1,13 @@
+# VariableDuplicatesValidator
+#
+# This validtor is designed for especially the following condition
+# - Use `accepts_nested_attributes_for :xxx` in a parent model
+# - Use `validates :xxx, uniqueness: { scope: :xxx_id }` in a child model
+class VariableDuplicatesValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ duplicates = value.reject(&:marked_for_destruction?).group_by(&:key).select { |_, v| v.many? }.map(&:first)
+ if duplicates.any?
+ record.errors.add(attribute, "Duplicate variables: #{duplicates.join(", ")}")
+ end
+ end
+end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 5f5eeb8b9a9..7f1e13c7989 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -333,6 +333,22 @@
Environment variable `prometheus_multiproc_dir` does not exist or is not pointing to a valid directory.
%fieldset
+ %legend Profiling - Performance Bar
+ %p
+ Enable the Performance Bar for a given group.
+ = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :performance_bar_enabled do
+ = f.check_box :performance_bar_enabled
+ Enable the Performance Bar
+ .form-group
+ = f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :performance_bar_allowed_group_id, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
+
+ %fieldset
%legend Background Jobs
%p
These settings require a restart to take effect.
diff --git a/app/views/projects/variables/_content.html.haml b/app/views/ci/variables/_content.html.haml
index 98f618ca3b8..98f618ca3b8 100644
--- a/app/views/projects/variables/_content.html.haml
+++ b/app/views/ci/variables/_content.html.haml
diff --git a/app/views/projects/variables/_form.html.haml b/app/views/ci/variables/_form.html.haml
index 0a70a301cb4..eebd0955c80 100644
--- a/app/views/projects/variables/_form.html.haml
+++ b/app/views/ci/variables/_form.html.haml
@@ -1,12 +1,12 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @variable] do |f|
+= form_for @variable, as: :variable, url: @variable.form_path do |f|
= form_errors(@variable)
.form-group
= f.label :key, "Key", class: "label-light"
- = f.text_field :key, class: "form-control", placeholder: "PROJECT_VARIABLE", required: true
+ = f.text_field :key, class: "form-control", placeholder: @variable.placeholder, required: true
.form-group
= f.label :value, "Value", class: "label-light"
- = f.text_area :value, class: "form-control", placeholder: "PROJECT_VARIABLE"
+ = f.text_area :value, class: "form-control", placeholder: @variable.placeholder
.form-group
.checkbox
= f.label :protected do
diff --git a/app/views/projects/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
index 5e6786f6698..007c2344b5a 100644
--- a/app/views/projects/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -1,16 +1,16 @@
.row.prepend-top-default.append-bottom-default
.col-lg-4
- = render "projects/variables/content"
+ = render "ci/variables/content"
.col-lg-8
%h5.prepend-top-0
Add a variable
- = render "projects/variables/form", btn_text: "Add new variable"
+ = render "ci/variables/form", btn_text: "Add new variable"
%hr
%h5.prepend-top-0
- Your variables (#{@project.variables.size})
- - if @project.variables.empty?
+ Your variables (#{@variables.size})
+ - if @variables.empty?
%p.settings-message.text-center.append-bottom-0
No variables found, add one with the form above.
- else
- = render "projects/variables/table"
+ = render "ci/variables/table"
%button.btn.btn-info.js-btn-toggle-reveal-values{ "data-status" => 'hidden' } Reveal Values
diff --git a/app/views/ci/variables/_show.html.haml b/app/views/ci/variables/_show.html.haml
new file mode 100644
index 00000000000..2bfb290629d
--- /dev/null
+++ b/app/views/ci/variables/_show.html.haml
@@ -0,0 +1,9 @@
+- page_title "Variables"
+
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ = render "ci/variables/content"
+ .col-lg-9
+ %h5.prepend-top-0
+ Update variable
+ = render "ci/variables/form", btn_text: "Save variable"
diff --git a/app/views/projects/variables/_table.html.haml b/app/views/ci/variables/_table.html.haml
index 4ce6a828812..71a0b56c4f4 100644
--- a/app/views/projects/variables/_table.html.haml
+++ b/app/views/ci/variables/_table.html.haml
@@ -11,18 +11,18 @@
%th Protected
%th
%tbody
- - @project.variables.order_key_asc.each do |variable|
+ - @variables.each do |variable|
- if variable.id?
%tr
%td.variable-key= variable.key
%td.variable-value{ "data-value" => variable.value }******
%td.variable-protected= Gitlab::Utils.boolean_to_yes_no(variable.protected)
%td.variable-menu
- = link_to project_variable_path(@project, variable), class: "btn btn-transparent btn-variable-edit" do
+ = link_to variable.edit_path, class: "btn btn-transparent btn-variable-edit" do
%span.sr-only
Update
= icon("pencil")
- = link_to project_variable_path(@project, variable), class: "btn btn-transparent btn-variable-delete", method: :delete, data: { confirm: "Are you sure?" } do
+ = link_to variable.delete_path, class: "btn btn-transparent btn-variable-delete", method: :delete, data: { confirm: "Are you sure?" } do
%span.sr-only
Remove
= icon("trash")
diff --git a/app/views/groups/_settings_head.html.haml b/app/views/groups/_settings_head.html.haml
index 2454e7355a7..623d233a46a 100644
--- a/app/views/groups/_settings_head.html.haml
+++ b/app/views/groups/_settings_head.html.haml
@@ -12,3 +12,8 @@
= link_to projects_group_path(@group), title: 'Projects' do
%span
Projects
+
+ = nav_link(controller: :ci_cd) do
+ = link_to group_settings_ci_cd_path(@group), title: 'Pipelines' do
+ %span
+ Pipelines
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/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
new file mode 100644
index 00000000000..bf36baf48ab
--- /dev/null
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -0,0 +1,4 @@
+- page_title "Pipelines"
+= render "groups/settings_head"
+
+= render 'ci/variables/index'
diff --git a/app/views/groups/variables/show.html.haml b/app/views/groups/variables/show.html.haml
new file mode 100644
index 00000000000..df533952b76
--- /dev/null
+++ b/app/views/groups/variables/show.html.haml
@@ -0,0 +1 @@
+= render 'ci/variables/show'
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 331d1181220..56e628a2b74 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -27,10 +27,11 @@
%td.shortcut
.key f
%td Focus Filter
- %tr
- %td.shortcut
- .key p b
- %td Show/hide the Performance Bar
+ - if performance_bar_enabled?
+ %tr
+ %td.shortcut
+ .key p b
+ %td Show/hide the Performance Bar
%tr
%td.shortcut
.key ?
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index abb6dc2e9f3..6ad22958df3 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -30,7 +30,7 @@
= stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print"
= stylesheet_link_tag "test", media: "all" if Rails.env.test?
- = stylesheet_link_tag 'peek' if peek_enabled?
+ = stylesheet_link_tag 'performance_bar' if performance_bar_enabled?
- if show_new_nav?
= stylesheet_link_tag "new_nav", media: "all"
@@ -44,7 +44,7 @@
= webpack_bundle_tag "main"
= webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled
= webpack_bundle_tag "test" if Rails.env.test?
- = webpack_bundle_tag 'peek' if peek_enabled?
+ = webpack_bundle_tag 'performance_bar' if performance_bar_enabled?
- if content_for?(:page_specific_javascripts)
= yield :page_specific_javascripts
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 1a9f5401a78..cc9219cb6fe 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -12,10 +12,12 @@
.content-wrapper{ class: "#{(layout_nav_class unless show_new_nav?)}" }
.alert-wrapper
= render "layouts/broadcast"
+ - if show_new_nav?
+ - if content_for?(:new_global_flash)
+ = yield :new_global_flash
+ = render "layouts/nav/breadcrumbs"
= render "layouts/flash"
= yield :flash_message
- - if show_new_nav?
- = render "layouts/nav/breadcrumbs"
%div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
.content{ id: "content-body" }
= yield
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index d879df8fc82..81f83b74826 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -3,7 +3,6 @@
= render "layouts/head"
%body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
= render "layouts/init_auto_complete" if @gfm_form
- = render 'peek/bar'
- if show_new_nav?
= render "layouts/header/new"
- else
@@ -11,3 +10,5 @@
= render 'layouts/page', sidebar: sidebar, nav: nav
= yield :scripts_body
+
+ = render 'peek/bar'
diff --git a/app/views/layouts/nav/_new_admin_sidebar.html.haml b/app/views/layouts/nav/_new_admin_sidebar.html.haml
index 40c1ca7b53e..d7a9e530983 100644
--- a/app/views/layouts/nav/_new_admin_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_admin_sidebar.html.haml
@@ -4,7 +4,7 @@
= icon('wrench')
.project-title Admin Area
%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
%span
Overview
@@ -26,7 +26,7 @@
= link_to admin_groups_path, title: 'Groups' do
%span
Groups
- = nav_link path: 'builds#index' do
+ = nav_link path: 'jobs#index' do
= link_to admin_jobs_path, title: 'Jobs' do
%span
Jobs
diff --git a/app/views/layouts/nav/_new_group_sidebar.html.haml b/app/views/layouts/nav/_new_group_sidebar.html.haml
index b7ac04cc3e5..7b68d11041e 100644
--- a/app/views/layouts/nav/_new_group_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_group_sidebar.html.haml
@@ -1,5 +1,5 @@
.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
= image_tag group_icon(@group), class: "avatar s40 avatar-tile"
.group-title
diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml
index 6e483353a2d..cc731db3cc1 100644
--- a/app/views/layouts/nav/_new_project_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_project_sidebar.html.haml
@@ -1,6 +1,6 @@
.nav-sidebar
- 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
= project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile')
.project-title
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/peek/views/_rblineprof.html.haml b/app/views/peek/views/_rblineprof.html.haml
new file mode 100644
index 00000000000..6c037930ca9
--- /dev/null
+++ b/app/views/peek/views/_rblineprof.html.haml
@@ -0,0 +1,7 @@
+Profile:
+
+= link_to 'all', url_for(lineprofiler: 'true'), class: 'js-toggle-modal-peek-line-profile'
+\/
+= link_to 'app & lib', url_for(lineprofiler: 'app'), class: 'js-toggle-modal-peek-line-profile'
+\/
+= link_to 'views', url_for(lineprofiler: 'views'), class: 'js-toggle-modal-peek-line-profile'
diff --git a/app/views/peek/views/_sql.html.haml b/app/views/peek/views/_sql.html.haml
index 16fc010f66f..dd8b524064f 100644
--- a/app/views/peek/views/_sql.html.haml
+++ b/app/views/peek/views/_sql.html.haml
@@ -1,13 +1,13 @@
%strong
- %a#peek-show-queries{ href: '#' }
+ %a.js-toggle-modal-peek-sql
%span{ data: { defer_to: "#{view.defer_key}-duration" } }...
\/
%span{ data: { defer_to: "#{view.defer_key}-calls" } }...
#modal-peek-pg-queries.modal{ tabindex: -1 }
- .modal-dialog
- #modal-peek-pg-queries-content.modal-content
+ .modal-dialog.modal-full
+ .modal-content
.modal-header
- %a.close{ href: "#", "data-dismiss" => "modal" } ×
+ %button.close.btn.btn-link.btn-sm{ type: 'button', data: { dismiss: 'modal' } } X
%h4
SQL queries
.modal-body{ data: { defer_to: "#{view.defer_key}-queries" } }...
diff --git a/app/views/projects/blob/viewers/_readme.html.haml b/app/views/projects/blob/viewers/_readme.html.haml
index 507f44d4745..d8492abc638 100644
--- a/app/views/projects/blob/viewers/_readme.html.haml
+++ b/app/views/projects/blob/viewers/_readme.html.haml
@@ -1,4 +1,4 @@
= icon('info-circle fw')
= succeed '.' do
To learn more about this project, read
- = link_to "the wiki", project_wikis_path(viewer.project)
+ = link_to "the wiki", get_project_wiki_path(viewer.project)
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 50e0bad3ccf..0f132a68ce1 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,6 +1,7 @@
- @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)
= render 'shared/no_ssh'
= render 'shared/no_password'
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index cf8493faba8..a57844f974e 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -30,24 +30,23 @@
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
- if can_update_issue
- %li
- = link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'issuable-edit'
- %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'
- %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'
+ %li= link_to 'Edit', edit_project_issue_path(@project, @issue)
+ - unless current_user == @issue.author
+ %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
+ - if can_update_issue
+ %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'
+ %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
- %li
- = link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
+ %li= 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
%li.divider
- %li
- = link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
+ %li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
- 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 '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
= 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
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 3182aecd0a8..a2e819fb3a7 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -1,3 +1,5 @@
+- can_update_merge_request = can?(current_user, :update_merge_request, @merge_request)
+
- if @merge_request.closed_without_fork?
.alert.alert-danger
%p The source project of this merge request has been removed.
@@ -15,21 +17,24 @@
.issuable-meta
= issuable_meta(@merge_request, @project, "Merge request")
- - if can?(current_user, :update_merge_request, @merge_request)
- .issuable-actions
- .clearfix.issue-btn-group.dropdown
- %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
- Options
- = icon('caret-down')
- .dropdown-menu.dropdown-menu-align-right.hidden-lg
- %ul
+ .issuable-actions
+ .clearfix.issue-btn-group.dropdown
+ %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
+ Options
+ = icon('caret-down')
+ .dropdown-menu.dropdown-menu-align-right.hidden-lg
+ %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) }
= 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) }
= 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'
- = 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 '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
- Edit
+
+ - if can_update_merge_request
+ = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit"
+
+ = render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request
diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml
index fc7fa5c1876..857ae00d0ab 100644
--- a/app/views/projects/pipeline_schedules/_form.html.haml
+++ b/app/views/projects/pipeline_schedules/_form.html.haml
@@ -24,6 +24,14 @@
= f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true
.form-group
.col-md-9
+ %label.label-light
+ #{ s_('PipelineSchedules|Variables') }
+ %ul.js-pipeline-variable-list.pipeline-variable-list
+ - @schedule.variables.each do |variable|
+ = render 'variable_row', id: variable.id, key: variable.key, value: variable.value
+ = render 'variable_row'
+ .form-group
+ .col-md-9
= f.label :active, s_('PipelineSchedules|Activated'), class: 'label-light'
%div
= f.check_box :active, required: false, value: @schedule.active?
diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
index 08ccd57c81a..97c0407a01d 100644
--- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
+++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
@@ -26,7 +26,7 @@
= pipeline_schedule.owner&.name
%td
.pull-right.btn-group
- - if can?(current_user, :update_pipeline_schedule, @project) && !pipeline_schedule.owned_by?(current_user)
+ - if can?(current_user, :update_pipeline_schedule, pipeline_schedule)
= link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('PipelineSchedules|Take ownership'), class: 'btn' do
= s_('PipelineSchedules|Take ownership')
- if can?(current_user, :update_pipeline_schedule, pipeline_schedule)
diff --git a/app/views/projects/pipeline_schedules/_variable_row.html.haml b/app/views/projects/pipeline_schedules/_variable_row.html.haml
new file mode 100644
index 00000000000..564cb5d1ca9
--- /dev/null
+++ b/app/views/projects/pipeline_schedules/_variable_row.html.haml
@@ -0,0 +1,17 @@
+- id = local_assigns.fetch(:id, nil)
+- key = local_assigns.fetch(:key, "")
+- value = local_assigns.fetch(:value, "")
+%li.js-row.pipeline-variable-row{ data: { is_persisted: "#{!id.nil?}" } }
+ .pipeline-variable-row-body
+ %input{ type: "hidden", name: "schedule[variables_attributes][][id]", value: id }
+ %input.js-destroy-input{ type: "hidden", name: "schedule[variables_attributes][][_destroy]" }
+ %input.js-user-input.pipeline-variable-key-input.form-control{ type: "text",
+ name: "schedule[variables_attributes][][key]",
+ value: key,
+ placeholder: s_('PipelineSchedules|Input variable key') }
+ %textarea.js-user-input.pipeline-variable-value-input.form-control{ rows: 1,
+ name: "schedule[variables_attributes][][value]",
+ placeholder: s_('PipelineSchedules|Input variable value') }
+ = value
+ %button.js-row-remove-button.pipeline-variable-row-remove-button{ 'aria-label': s_('PipelineSchedules|Remove variable row') }
+ %i.fa.fa-minus-circle{ 'aria-hidden': "true" }
diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml
deleted file mode 100644
index c7996077bc7..00000000000
--- a/app/views/projects/project_members/_group_members.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-.panel.panel-default
- .panel-heading
- Group members with access to
- %strong= @group.name
- %span.badge= members.size
- - if can?(current_user, :admin_group_member, @group)
- .controls
- = link_to 'Manage group members',
- group_group_members_path(@group),
- class: 'btn'
- %ul.content-list
- = render partial: 'shared/members/member',
- collection: members.limit(20),
- as: :member,
- locals: { show_controls: false }
- - if members.size > 20
- %li
- and #{members.count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(@group)}
diff --git a/app/views/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml
deleted file mode 100644
index 7902ddb1ae9..00000000000
--- a/app/views/projects/project_members/_shared_group_members.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-- @project_group_links.each do |group_links|
- - shared_group = group_links.group
- - shared_group_members = shared_group.members
- - shared_group_users_count = shared_group_members.size
- .panel.panel-default
- .panel-heading
- Shared with
- %strong= shared_group.name
- group, members with
- %strong= group_links.human_access
- role (#{shared_group_users_count})
- - if can?(current_user, :admin_group, shared_group)
- .panel-head-actions
- = link_to group_group_members_path(shared_group), class: 'btn btn-sm' do
- %i.fa.fa-pencil-square-o
- Edit group members
- %ul.content-list
- = render partial: 'shared/members/member',
- collection: shared_group_members.order(access_level: :desc).limit(20),
- as: :member,
- locals: { show_controls: false, show_roles: false }
- - if shared_group_users_count > 20
- %li
- and #{shared_group_users_count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(shared_group)}
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index 3fec9ea7310..e71d58ec26d 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/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 00ccc3ec41e..6afb38c5709 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -3,6 +3,6 @@
= render "projects/settings/head"
= render 'projects/runners/index'
-= render 'projects/variables/index'
+= render 'ci/variables/index'
= render 'projects/triggers/index'
= render 'projects/pipelines_settings/show'
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index ea780b1cb83..d413c4619be 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,9 +1,10 @@
- @no_container = true
+- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity")
-= content_for :flash_message do
+= content_for flash_message_container do
- if current_user && can?(current_user, :download_code, @project)
= render 'shared/no_ssh'
= render 'shared/no_password'
diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml
index 297a53ca98c..df533952b76 100644
--- a/app/views/projects/variables/show.html.haml
+++ b/app/views/projects/variables/show.html.haml
@@ -1,9 +1 @@
-- page_title "Variables"
-
-.row.prepend-top-default.append-bottom-default
- .col-lg-3
- = render "content"
- .col-lg-9
- %h5.prepend-top-0
- Update variable
- = render "form", btn_text: "Save variable"
+= render 'ci/variables/show'
diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml
new file mode 100644
index 00000000000..8a1268a1c6d
--- /dev/null
+++ b/app/views/shared/issuable/_close_reopen_button.html.haml
@@ -0,0 +1,14 @@
+- is_current_user = issuable_author_is_current_user(issuable)
+- display_issuable_type = issuable_display_type(issuable)
+- button_method = issuable_close_reopen_button_method(issuable)
+
+- if can_update && is_current_user
+ = link_to "Close #{display_issuable_type}", close_issuable_url(issuable), method: button_method,
+ class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
+ = link_to "Reopen #{display_issuable_type}", reopen_issuable_url(issuable), method: button_method,
+ class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
+- elsif can_update && !is_current_user
+ = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
+- else
+ = link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
+ class: 'hidden-xs hidden-sm btn btn-grouped btn-close-color', title: 'Report abuse'
diff --git a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
new file mode 100644
index 00000000000..6756a7f17fd
--- /dev/null
+++ b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
@@ -0,0 +1,49 @@
+- display_issuable_type = issuable_display_type(issuable)
+- button_action = issuable.closed? ? 'reopen' : 'close'
+- display_button_action = button_action.capitalize
+- button_responsive_class = 'hidden-xs hidden-sm'
+- button_class = "#{button_responsive_class} btn btn-grouped js-issuable-close-button issuable-close-button"
+- toggle_class = "#{button_responsive_class} btn btn-nr dropdown-toggle js-issuable-close-toggle"
+- button_method = issuable_close_reopen_button_method(issuable)
+
+.pull-left.btn-group.prepend-left-10.issuable-close-dropdown.droplab-dropdown.js-issuable-close-dropdown
+ = link_to "#{display_button_action} #{display_issuable_type}", close_reopen_issuable_url(issuable),
+ method: button_method, class: "#{button_class} btn-#{button_action}", title: "#{display_button_action} #{display_issuable_type}"
+
+ = button_tag type: 'button', class: "#{toggle_class} btn-#{button_action}-color",
+ data: { 'dropdown-trigger' => '#issuable-close-menu' }, 'aria-label' => 'Toggle dropdown' do
+ = icon('caret-down', class: 'toggle-icon icon')
+
+ %ul#issuable-close-menu.js-issuable-close-menu.dropdown-menu{ class: button_responsive_class, data: { dropdown: true } }
+ %li.close-item{ class: "#{issuable_button_visibility(issuable, true) || 'droplab-item-selected'}",
+ data: { text: "Close #{display_issuable_type}", url: close_issuable_url(issuable),
+ button_class: "#{button_class} btn-close", toggle_class: "#{toggle_class} btn-close-color", method: button_method } }
+ %button.btn.btn-transparent
+ = icon('check', class: 'icon')
+ .description
+ %strong.title
+ Close
+ = display_issuable_type
+
+ %li.reopen-item{ class: "#{issuable_button_visibility(issuable, false) || 'droplab-item-selected'}",
+ data: { text: "Reopen #{display_issuable_type}", url: reopen_issuable_url(issuable),
+ button_class: "#{button_class} btn-reopen", toggle_class: "#{toggle_class} btn-reopen-color", method: button_method } }
+ %button.btn.btn-transparent
+ = icon('check', class: 'icon')
+ .description
+ %strong.title
+ Reopen
+ = display_issuable_type
+
+ %li.divider.droplab-item-ignore
+
+ %li.report-item{ data: { text: 'Report abuse', url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
+ button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } }
+ %button.btn.btn-transparent
+ = icon('check', class: 'icon')
+ .description
+ %strong.title Report abuse
+ %p.text
+ Report
+ = display_issuable_type.pluralize
+ that are abusive, inappropriate or spam.
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index ae890567225..bdb573cb8fd 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -19,7 +19,7 @@
content_class: "filtered-search-history-dropdown-content",
title: "Recent searches" }) do
.js-filtered-search-history-dropdown{ data: { project_full_path: @project.full_path } }
- .filtered-search-box-input-container
+ .filtered-search-box-input-container.droplab-dropdown
.scroll-container
%ul.tokens-container.list-unstyled
%li.input-token
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} &middot;
@@ -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 @@
&middot;
= 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
diff --git a/app/views/shared/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml
index 29cf5825292..1dfe380db16 100644
--- a/app/views/shared/notes/_comment_button.html.haml
+++ b/app/views/shared/notes/_comment_button.html.haml
@@ -1,6 +1,6 @@
- noteable_name = @note.noteable.human_class_name
-.pull-left.btn-group.append-right-10.comment-type-dropdown.js-comment-type-dropdown
+.pull-left.btn-group.append-right-10.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown
%input.btn.btn-nr.btn-create.comment-btn.js-comment-button.js-comment-submit-button{ type: 'submit', value: 'Comment' }
- if @note.can_be_discussion_note?
@@ -9,8 +9,8 @@
%ul#resolvable-comment-menu.dropdown-menu{ data: { dropdown: true } }
%li#comment.droplab-item-selected{ data: { value: '', 'submit-text' => 'Comment', 'close-text' => "Comment & close #{noteable_name}", 'reopen-text' => "Comment & reopen #{noteable_name}" } }
- %a{ href: '#' }
- = icon('check')
+ %button.btn.btn-transparent
+ = icon('check', class: 'icon')
.description
%strong Comment
%p
@@ -19,8 +19,8 @@
%li.divider.droplab-item-ignore
%li#discussion{ data: { value: 'DiscussionNote', 'submit-text' => 'Start discussion', 'close-text' => "Start discussion & close #{noteable_name}", 'reopen-text' => "Start discussion & reopen #{noteable_name}" } }
- %a{ href: '#' }
- = icon('check')
+ %button.btn.btn-transparent
+ = icon('check', class: 'icon')
.description
%strong Start discussion
%p