diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2017-09-06 13:18:53 +0100 |
---|---|---|
committer | Filipa Lacerda <filipa@gitlab.com> | 2017-09-06 13:18:53 +0100 |
commit | 0e1404d441b0db532046d50dcbaf754ff4d3a77e (patch) | |
tree | 87a5b482c31e846d5e799291aa9852a008732c2a /app | |
parent | fa0f9d60e2be51261f4f58c25d75229ba996659d (diff) | |
parent | ba302454e1191f28453929df80e13c4dd418f8c7 (diff) | |
download | gitlab-ce-collapsable-pipeline-settings.tar.gz |
Merge branch 'master' into collapsable-pipeline-settingscollapsable-pipeline-settings
* master: (260 commits)
Enable auto-retry in GitLab CI/CD pipeline
Clean up new navigation templates
Wait for gitaly to boot during tests
Update 'Visibility of pipelines'
refactored code
Fix note resolution specs
Add author and MR to changelog
Tidy up projects API specs
Resolve outdated diff discussions on push
Fix migration
change collapse to resolve and comments to discussions
add unit tests for new collapse_outdated_diff_comments toggle
Add functionality to collapse outdated diff comments regardless of discussion resolution
refactor code based on feedback
fix spec failures
Use flexbox for prometheus graph row grouping instead of bootstrap classes
Fix wrong API status codes
small refactor
Hide admin link from default search results for non-admins
Make search dropdowns consistent
...
Diffstat (limited to 'app')
224 files changed, 2644 insertions, 1932 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 78cb3def879..8acddd6194c 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -5,7 +5,7 @@ const Api = { groupPath: '/api/:version/groups/:id.json', namespacesPath: '/api/:version/namespaces.json', groupProjectsPath: '/api/:version/groups/:id/projects.json', - projectsPath: '/api/:version/projects.json?simple=true', + projectsPath: '/api/:version/projects.json', labelsPath: '/:namespace_path/:project_path/labels', licensePath: '/api/:version/templates/licenses/:key', gitignorePath: '/api/:version/templates/gitignores/:key', @@ -58,6 +58,7 @@ const Api = { const defaults = { search: query, per_page: 20, + simple: true, }; if (gon.current_user_id) { diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js index 6db8b3afbef..768453b28f1 100644 --- a/app/assets/javascripts/commons/index.js +++ b/app/assets/javascripts/commons/index.js @@ -2,3 +2,4 @@ import 'underscore'; import './polyfills'; import './jquery'; import './bootstrap'; +import './vue'; diff --git a/app/assets/javascripts/vue_shared/common_vue.js b/app/assets/javascripts/commons/vue.js index eb2a6071fda..8b62d78c043 100644 --- a/app/assets/javascripts/vue_shared/common_vue.js +++ b/app/assets/javascripts/commons/vue.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import './vue_resource_interceptor'; if (process.env.NODE_ENV !== 'production') { Vue.config.productionTip = false; diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js index c37249c060a..06ce84d7599 100644 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -21,11 +21,13 @@ const DiffNoteAvatars = Vue.extend({ }, template: ` <div class="diff-comment-avatar-holders" + :class="discussionClassName" v-show="notesCount !== 0"> <div v-if="!isVisible"> <!-- FIXME: Pass an alt attribute here for accessibility --> <user-avatar-image v-for="note in notesSubset" + :key="note.id" class="diff-comment-avatar js-diff-comment-avatar" @click.native="clickedAvatar($event)" :img-src="note.authorAvatar" @@ -68,7 +70,8 @@ const DiffNoteAvatars = Vue.extend({ }); }); }, - destroyed() { + beforeDestroy() { + this.addNoCommentClass(); $(document).off('toggle.comments'); }, watch: { @@ -85,6 +88,9 @@ const DiffNoteAvatars = Vue.extend({ }, }, computed: { + discussionClassName() { + return `js-diff-avatars-${this.discussionId}`; + }, notesSubset() { let notes = []; diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js index 5decfc1dc01..0863c3406bd 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -32,6 +32,10 @@ $(() => { const tmpApp = new tmp().$mount(); $(this).replaceWith(tmpApp.$el); + $(tmpApp.$el).one('remove.vue', () => { + tmpApp.$destroy(); + tmpApp.$el.remove(); + }); }); const $components = $(COMPONENT_SELECTOR).filter(function () { diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js new file mode 100644 index 00000000000..800ca05cd11 --- /dev/null +++ b/app/assets/javascripts/feature_highlight/feature_highlight.js @@ -0,0 +1,61 @@ +import Cookies from 'js-cookie'; +import _ from 'underscore'; +import { + getCookieName, + getSelector, + hidePopover, + setupDismissButton, + mouseenter, + mouseleave, +} from './feature_highlight_helper'; + +export const setupFeatureHighlightPopover = (id, debounceTimeout = 300) => { + const $selector = $(getSelector(id)); + const $parent = $selector.parent(); + const $popoverContent = $parent.siblings('.feature-highlight-popover-content'); + const hideOnScroll = hidePopover.bind($selector); + const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout); + + $selector + // Setup popover + .data('content', $popoverContent.prop('outerHTML')) + .popover({ + html: true, + // Override the existing template to add custom CSS classes + template: ` + <div class="popover feature-highlight-popover" role="tooltip"> + <div class="arrow"></div> + <div class="popover-content"></div> + </div> + `, + }) + .on('mouseenter', mouseenter) + .on('mouseleave', debouncedMouseleave) + .on('inserted.bs.popover', setupDismissButton) + .on('show.bs.popover', () => { + window.addEventListener('scroll', hideOnScroll); + }) + .on('hide.bs.popover', () => { + window.removeEventListener('scroll', hideOnScroll); + }) + // Display feature highlight + .removeAttr('disabled'); +}; + +export const shouldHighlightFeature = (id) => { + const element = document.querySelector(getSelector(id)); + const previouslyDismissed = Cookies.get(getCookieName(id)) === 'true'; + + return element && !previouslyDismissed; +}; + +export const highlightFeatures = (highlightOrder) => { + const featureId = highlightOrder.find(shouldHighlightFeature); + + if (featureId) { + setupFeatureHighlightPopover(featureId); + return true; + } + + return false; +}; diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js new file mode 100644 index 00000000000..9f741355cd7 --- /dev/null +++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js @@ -0,0 +1,57 @@ +import Cookies from 'js-cookie'; + +export const getCookieName = cookieId => `feature-highlighted-${cookieId}`; +export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`; + +export const showPopover = function showPopover() { + if (this.hasClass('js-popover-show')) { + return false; + } + this.popover('show'); + this.addClass('disable-animation js-popover-show'); + + return true; +}; + +export const hidePopover = function hidePopover() { + if (!this.hasClass('js-popover-show')) { + return false; + } + this.popover('hide'); + this.removeClass('disable-animation js-popover-show'); + + return true; +}; + +export const dismiss = function dismiss(cookieId) { + Cookies.set(getCookieName(cookieId), true); + hidePopover.call(this); + this.hide(); +}; + +export const mouseleave = function mouseleave() { + if (!$('.popover:hover').length > 0) { + const $featureHighlight = $(this); + hidePopover.call($featureHighlight); + } +}; + +export const mouseenter = function mouseenter() { + const $featureHighlight = $(this); + + const showedPopover = showPopover.call($featureHighlight); + if (showedPopover) { + $('.popover') + .on('mouseleave', mouseleave.bind($featureHighlight)); + } +}; + +export const setupDismissButton = function setupDismissButton() { + const popoverId = this.getAttribute('aria-describedby'); + const cookieId = this.dataset.highlight; + const $popover = $(this); + const dismissWrapper = dismiss.bind($popover, cookieId); + + $(`#${popoverId} .dismiss-feature-highlight`) + .on('click', dismissWrapper); +}; diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_options.js b/app/assets/javascripts/feature_highlight/feature_highlight_options.js new file mode 100644 index 00000000000..fd48f2e87cc --- /dev/null +++ b/app/assets/javascripts/feature_highlight/feature_highlight_options.js @@ -0,0 +1,12 @@ +import { highlightFeatures } from './feature_highlight'; +import bp from '../breakpoints'; + +const highlightOrder = ['issue-boards']; + +export default function domContentLoaded(order) { + if (bp.getBreakpointSize() === 'lg') { + highlightFeatures(order); + } +} + +document.addEventListener('DOMContentLoaded', domContentLoaded.bind(this, highlightOrder)); diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index 1c5ca1d3cf9..23040cd9eb8 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -61,7 +61,7 @@ class DropdownHint extends gl.FilteredSearchDropdown { .map(tokenKey => ({ icon: `fa-${tokenKey.icon}`, hint: tokenKey.key, - tag: `<${tokenKey.tag}>`, + tag: `:${tokenKey.tag}`, type: tokenKey.type, })); diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index b62acfcd445..6f7671aa6fe 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -486,7 +486,7 @@ GitLabDropdown = (function() { GitLabDropdown.prototype.shouldPropagate = function(e) { var $target; - if (this.options.multiSelect) { + if (this.options.multiSelect || this.options.shouldPropagate === false) { $target = $(e.target); if ($target && !$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && @@ -546,10 +546,10 @@ GitLabDropdown = (function() { }; GitLabDropdown.prototype.positionMenuAbove = function() { - var $button = $(this.el); var $menu = this.dropdown.find('.dropdown-menu'); - $menu.css('top', ($button.height() + $menu.height()) * -1); + $menu.css('top', 'initial'); + $menu.css('bottom', '100%'); }; GitLabDropdown.prototype.hidden = function(e) { @@ -637,11 +637,15 @@ GitLabDropdown = (function() { value = this.options.id ? this.options.id(data) : data.id; fieldName = this.options.fieldName; - if (value) { value = value.toString().replace(/'/g, '\\\''); } - - field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); - if (field.length) { - selected = true; + if (value) { + value = value.toString().replace(/'/g, '\\\''); + field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`); + if (field.length) { + selected = true; + } + } else { + field = this.dropdown.parent().find(`input[name='${fieldName}']`); + selected = !field.length; } } // Set URL @@ -698,7 +702,7 @@ GitLabDropdown = (function() { GitLabDropdown.prototype.noResults = function() { var html; - return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>"; + return html = '<li class="dropdown-menu-empty-link"><a href="#" class="is-focused">No matching results</a></li>'; }; GitLabDropdown.prototype.rowClicked = function(el) { diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js index 3e483b69fd2..6677391c689 100644 --- a/app/assets/javascripts/group_name.js +++ b/app/assets/javascripts/group_name.js @@ -1,4 +1,3 @@ -import Cookies from 'js-cookie'; import _ from 'underscore'; export default class GroupName { @@ -39,17 +38,9 @@ export default class GroupName { this.toggle.setAttribute('type', 'button'); this.toggle.className = 'text-expander group-name-toggle'; this.toggle.setAttribute('aria-label', 'Toggle full path'); - if (Cookies.get('new_nav') === 'true') { - this.toggle.innerHTML = '<i class="fa fa-ellipsis-h" aria-hidden="true"></i>'; - } else { - this.toggle.innerHTML = '...'; - } + this.toggle.innerHTML = '<i class="fa fa-ellipsis-h" aria-hidden="true"></i>'; this.toggle.addEventListener('click', this.toggleGroups.bind(this)); - if (Cookies.get('new_nav') === 'true') { - this.title.insertBefore(this.toggle, this.groupTitle); - } else { - this.titleContainer.insertBefore(this.toggle, this.title); - } + this.title.insertBefore(this.toggle, this.groupTitle); this.toggleGroups(); } diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index d314f3c4d43..0e8a0519928 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -5,7 +5,6 @@ /* global SubscriptionSelect */ import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; -import SidebarHeightManager from './sidebar_height_manager'; const HIDDEN_CLASS = 'hidden'; const DISABLED_CONTENT_CLASS = 'disabled-content'; @@ -50,13 +49,6 @@ export default class IssuableBulkUpdateSidebar { new SubscriptionSelect(); } - getNavHeight() { - const navbarHeight = $('.navbar-gitlab').outerHeight(); - const layoutNavHeight = $('.layout-nav').outerHeight(); - const subNavScroll = $('.sub-nav-scroll').outerHeight(); - return navbarHeight + layoutNavHeight + subNavScroll; - } - setupBulkUpdateActions() { IssuableBulkUpdateActions.setOriginalDropdownData(); } @@ -84,23 +76,6 @@ export default class IssuableBulkUpdateSidebar { this.toggleBulkEditButtonDisabled(enable); this.toggleOtherFiltersDisabled(enable); this.toggleCheckboxDisplay(enable); - - if (enable) { - this.initAffix(); - SidebarHeightManager.init(); - } - } - - initAffix() { - if (!this.$sidebar.hasClass('affix-top')) { - const offsetTop = $('.scrolling-tabs-container').outerHeight() + $('.sub-nav-scroll').outerHeight(); - - this.$sidebar.affix({ - offset: { - top: offsetTop, - }, - }); - } } updateSelectedIssuableIds() { diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 3f848e0859b..470c39c6f76 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -10,8 +10,6 @@ import ZenMode from './zen_mode'; (function() { this.IssuableForm = (function() { - IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?'; - IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i; function IssuableForm(form) { @@ -26,7 +24,6 @@ import ZenMode from './zen_mode'; new ZenMode(); this.titleField = this.form.find("input[name*='[title]']"); this.descriptionField = this.form.find("textarea[name*='[description]']"); - this.issueMoveField = this.form.find("#move_to_project_id"); if (!(this.titleField.length && this.descriptionField.length)) { return; } @@ -34,7 +31,6 @@ import ZenMode from './zen_mode'; this.form.on("submit", this.handleSubmit); this.form.on("click", ".btn-cancel", this.resetAutosave); this.initWip(); - this.initMoveDropdown(); $issuableDueDate = $('#issuable-due-date'); if ($issuableDueDate.length) { calendar = new Pikaday({ @@ -56,12 +52,6 @@ import ZenMode from './zen_mode'; }; IssuableForm.prototype.handleSubmit = function() { - var fieldId = (this.issueMoveField != null) ? this.issueMoveField.val() : null; - if ((parseInt(fieldId, 10) || 0) > 0) { - if (!confirm(this.issueMoveConfirmMsg)) { - return false; - } - } return this.resetAutosave(); }; @@ -113,48 +103,6 @@ import ZenMode from './zen_mode'; return this.titleField.val("WIP: " + (this.titleField.val())); }; - IssuableForm.prototype.initMoveDropdown = function() { - var $moveDropdown, pageSize; - $moveDropdown = $('.js-move-dropdown'); - if ($moveDropdown.length) { - pageSize = $moveDropdown.data('page-size'); - return $('.js-move-dropdown').select2({ - ajax: { - url: $moveDropdown.data('projects-url'), - quietMillis: 125, - data: function(term, page, context) { - return { - search: term, - offset_id: context - }; - }, - results: function(data) { - var context, - more; - - if (data.length >= pageSize) - more = true; - - if (data[data.length - 1]) - context = data[data.length - 1].id; - - return { - results: data, - more: more, - context: context - }; - } - }, - formatResult: function(project) { - return project.name_with_namespace; - }, - formatSelection: function(project) { - return project.name_with_namespace; - } - }); - } - }; - return IssuableForm; })(); }).call(window); diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index eaaafd4c149..e115ee40219 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -17,10 +17,6 @@ export default { required: true, type: String, }, - canMove: { - required: true, - type: Boolean, - }, canUpdate: { required: true, type: Boolean, @@ -96,10 +92,6 @@ export default { type: String, required: true, }, - projectsAutocompletePath: { - type: String, - required: true, - }, }, data() { const store = new Store({ @@ -142,7 +134,6 @@ export default { confidential: this.isConfidential, description: this.state.descriptionText, lockedWarningVisible: false, - move_to_project_id: 0, updateLoading: false, }); } @@ -151,16 +142,6 @@ export default { this.showForm = false; }, updateIssuable() { - const canPostUpdate = this.store.formState.move_to_project_id !== 0 ? - confirm('Are you sure you want to move this issue to another project?') : true; // eslint-disable-line no-alert - - if (!canPostUpdate) { - this.store.setFormState({ - updateLoading: false, - }); - return; - } - this.service.updateIssuable(this.store.formState) .then(res => res.json()) .then((data) => { @@ -239,14 +220,12 @@ export default { <form-component v-if="canUpdate && showForm" :form-state="formState" - :can-move="canMove" :can-destroy="canDestroy" :issuable-templates="issuableTemplates" :markdown-docs-path="markdownDocsPath" :markdown-preview-path="markdownPreviewPath" :project-path="projectPath" :project-namespace="projectNamespace" - :projects-autocomplete-path="projectsAutocompletePath" /> <div v-else> <title-component diff --git a/app/assets/javascripts/issue_show/components/fields/project_move.vue b/app/assets/javascripts/issue_show/components/fields/project_move.vue deleted file mode 100644 index e514bebc5f6..00000000000 --- a/app/assets/javascripts/issue_show/components/fields/project_move.vue +++ /dev/null @@ -1,83 +0,0 @@ -<script> - import tooltip from '../../../vue_shared/directives/tooltip'; - - export default { - directives: { - tooltip, - }, - props: { - formState: { - type: Object, - required: true, - }, - projectsAutocompletePath: { - type: String, - required: true, - }, - }, - mounted() { - const $moveDropdown = $(this.$refs['move-dropdown']); - - $moveDropdown.select2({ - ajax: { - url: this.projectsAutocompletePath, - quietMillis: 125, - data(term, page, context) { - return { - search: term, - offset_id: context, - }; - }, - results(data) { - const more = data.length >= 50; - const context = data[data.length - 1] ? data[data.length - 1].id : null; - - return { - results: data, - more, - context, - }; - }, - }, - formatResult(project) { - return project.name_with_namespace; - }, - formatSelection(project) { - return project.name_with_namespace; - }, - }) - .on('change', (e) => { - this.formState.move_to_project_id = parseInt(e.target.value, 10); - }); - }, - beforeDestroy() { - $(this.$refs['move-dropdown']).select2('destroy'); - }, - }; -</script> - -<template> - <fieldset> - <label - for="issuable-move" - class="sr-only"> - Move - </label> - <div class="issuable-form-select-holder append-right-5"> - <input - ref="move-dropdown" - type="hidden" - id="issuable-move" - data-placeholder="Move to a different project" /> - </div> - <span - v-tooltip - data-placement="auto top" - title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location."> - <i - class="fa fa-question-circle" - aria-hidden="true"> - </i> - </span> - </fieldset> -</template> diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue index d9b53bc55cf..6a2dd502fe2 100644 --- a/app/assets/javascripts/issue_show/components/form.vue +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -4,15 +4,10 @@ import descriptionField from './fields/description.vue'; import editActions from './edit_actions.vue'; import descriptionTemplate from './fields/description_template.vue'; - import projectMove from './fields/project_move.vue'; import confidentialCheckbox from './fields/confidential_checkbox.vue'; export default { props: { - canMove: { - type: Boolean, - required: true, - }, canDestroy: { type: Boolean, required: true, @@ -42,10 +37,6 @@ type: String, required: true, }, - projectsAutocompletePath: { - type: String, - required: true, - }, }, components: { lockedWarning, @@ -53,7 +44,6 @@ descriptionField, descriptionTemplate, editActions, - projectMove, confidentialCheckbox, }, computed: { @@ -93,10 +83,6 @@ :markdown-docs-path="markdownDocsPath" /> <confidential-checkbox :form-state="formState" /> - <project-move - v-if="canMove" - :form-state="formState" - :projects-autocomplete-path="projectsAutocompletePath" /> <edit-actions :form-state="formState" :can-destroy="canDestroy" /> diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index 60b69b300fd..8053ef57e6c 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -28,7 +28,6 @@ document.addEventListener('DOMContentLoaded', () => { props: { canUpdate: this.canUpdate, canDestroy: this.canDestroy, - canMove: this.canMove, endpoint: this.endpoint, issuableRef: this.issuableRef, initialTitleHtml: this.initialTitleHtml, @@ -41,7 +40,6 @@ document.addEventListener('DOMContentLoaded', () => { markdownDocsPath: this.markdownDocsPath, projectPath: this.projectPath, projectNamespace: this.projectNamespace, - projectsAutocompletePath: this.projectsAutocompletePath, updatedAt: this.updatedAt, updatedByName: this.updatedByName, updatedByPath: this.updatedByPath, diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js index 0c8bd6f1cc3..f4639e9ed2a 100644 --- a/app/assets/javascripts/issue_show/stores/index.js +++ b/app/assets/javascripts/issue_show/stores/index.js @@ -6,7 +6,6 @@ export default class Store { confidential: false, description: '', lockedWarningVisible: false, - move_to_project_id: 0, updateLoading: false, }; } diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index 5c1ba416a03..d064a2c0024 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -50,19 +50,10 @@ import initFlyOutNav from './fly_out_nav'; }); }); - function applyScrollNavClass() { - const scrollOpacityHeight = 40; - $('.navbar-border').css('opacity', Math.min($(window).scrollTop() / scrollOpacityHeight, 1)); - } - $(() => { - if (Cookies.get('new_nav') === 'true') { - const newNavSidebar = new NewNavSidebar(); - newNavSidebar.bindEvents(); - - initFlyOutNav(); - } + const newNavSidebar = new NewNavSidebar(); + newNavSidebar.bindEvents(); - $(window).on('scroll', _.throttle(applyScrollNavClass, 100)); + initFlyOutNav(); }); }).call(window); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 6d7c7e3c930..f14458c8d41 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -102,6 +102,7 @@ import './label_manager'; import './labels'; import './labels_select'; import './layout_nav'; +import './feature_highlight/feature_highlight_options'; import LazyLoader from './lazy_loader'; import './line_highlighter'; import './logo'; @@ -131,6 +132,7 @@ import './project_new'; import './project_select'; import './project_show'; import './project_variables'; +import './projects_dropdown'; import './projects_list'; import './syntax_highlight'; import './render_math'; @@ -248,7 +250,10 @@ $(function () { // Initialize popovers $body.popover({ selector: '[data-toggle="popover"]', - trigger: 'focus' + trigger: 'focus', + // set the viewport to the main content, excluding the navigation bar, so + // the navigation can't overlap the popover + viewport: '.page-with-sidebar' }); $('.trigger-submit').on('change', function () { return $(this).parents('form').submit(); diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 74244faa5d9..b596c4f383f 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -4,7 +4,7 @@ import statusCodes from '../../lib/utils/http_status'; import MonitoringService from '../services/monitoring_service'; import GraphGroup from './graph_group.vue'; - import GraphRow from './graph_row.vue'; + import Graph from './graph.vue'; import EmptyState from './empty_state.vue'; import MonitoringStore from '../stores/monitoring_store'; import eventHub from '../event_hub'; @@ -32,8 +32,8 @@ }, components: { + Graph, GraphGroup, - GraphRow, EmptyState, }, @@ -127,10 +127,10 @@ :key="index" :name="groupData.group" > - <graph-row - v-for="(row, index) in groupData.metrics" + <graph + v-for="(graphData, index) in groupData.metrics" :key="index" - :row-data="row" + :graph-data="graphData" :update-aspect-ratio="updateAspectRatio" :deployment-data="store.deploymentData" /> diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index 6f6da9e1463..cde2ff7ca2a 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -3,11 +3,12 @@ import GraphLegend from './graph/legend.vue'; import GraphFlag from './graph/flag.vue'; import GraphDeployment from './graph/deployment.vue'; + import monitoringPaths from './monitoring_paths.vue'; import MonitoringMixin from '../mixins/monitoring_mixins'; import eventHub from '../event_hub'; import measurements from '../utils/measurements'; - import { formatRelevantDigits } from '../../lib/utils/number_utils'; import { timeScaleFormat } from '../utils/date_time_formatters'; + import createTimeSeries from '../utils/multiple_time_series'; import bp from '../../breakpoints'; const bisectDate = d3.bisector(d => d.time).left; @@ -18,10 +19,6 @@ type: Object, required: true, }, - classType: { - type: String, - required: true, - }, updateAspectRatio: { type: Boolean, required: true, @@ -36,32 +33,29 @@ data() { return { + baseGraphHeight: 450, + baseGraphWidth: 600, graphHeight: 450, graphWidth: 600, graphHeightOffset: 120, - xScale: {}, - yScale: {}, margin: {}, - data: [], unitOfDisplay: '', areaColorRgb: '#8fbce8', lineColorRgb: '#1f78d1', yAxisLabel: '', legendTitle: '', reducedDeploymentData: [], - area: '', - line: '', measurements: measurements.large, currentData: { time: new Date(), value: 0, }, - currentYCoordinate: 0, + currentDataIndex: 0, currentXCoordinate: 0, currentFlagPosition: 0, - metricUsage: '', showFlag: false, showDeployInfo: true, + timeSeries: [], }; }, @@ -69,16 +63,17 @@ GraphLegend, GraphFlag, GraphDeployment, + monitoringPaths, }, computed: { outterViewBox() { - return `0 0 ${this.graphWidth} ${this.graphHeight}`; + return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`; }, innerViewBox() { - if ((this.graphWidth - 150) > 0) { - return `0 0 ${this.graphWidth - 150} ${this.graphHeight}`; + if ((this.baseGraphWidth - 150) > 0) { + return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`; } return '0 0 0 0'; }, @@ -89,7 +84,7 @@ paddingBottomRootSvg() { return { - paddingBottom: `${(Math.ceil(this.graphHeight * 100) / this.graphWidth) || 0}%`, + paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`, }; }, }, @@ -104,17 +99,16 @@ this.margin = measurements.small.margin; this.measurements = measurements.small; } - this.data = query.result[0].values; this.unitOfDisplay = query.unit || ''; this.yAxisLabel = this.graphData.y_label || 'Values'; this.legendTitle = query.label || 'Average'; this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right; this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; - if (this.data !== undefined) { - this.renderAxesPaths(); - this.formatDeployments(); - } + this.baseGraphHeight = this.graphHeight; + this.baseGraphWidth = this.graphWidth; + this.renderAxesPaths(); + this.formatDeployments(); }, handleMouseOverGraph(e) { @@ -123,16 +117,17 @@ point.y = e.clientY; point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse()); point.x = point.x += 7; - const timeValueOverlay = this.xScale.invert(point.x); - const overlayIndex = bisectDate(this.data, timeValueOverlay, 1); - const d0 = this.data[overlayIndex - 1]; - const d1 = this.data[overlayIndex]; + const firstTimeSeries = this.timeSeries[0]; + const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x); + const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1); + const d0 = firstTimeSeries.values[overlayIndex - 1]; + const d1 = firstTimeSeries.values[overlayIndex]; if (d0 === undefined || d1 === undefined) return; const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay; this.currentData = evalTime ? d1 : d0; - this.currentXCoordinate = Math.floor(this.xScale(this.currentData.time)); + this.currentDataIndex = evalTime ? overlayIndex : (overlayIndex - 1); + this.currentXCoordinate = Math.floor(firstTimeSeries.timeSeriesScaleX(this.currentData.time)); const currentDeployXPos = this.mouseOverDeployInfo(point.x); - this.currentYCoordinate = this.yScale(this.currentData.value); if (this.currentXCoordinate > (this.graphWidth - 200)) { this.currentFlagPosition = this.currentXCoordinate - 103; @@ -145,17 +140,25 @@ } else { this.showFlag = true; } - - this.metricUsage = `${formatRelevantDigits(this.currentData.value)} ${this.unitOfDisplay}`; }, renderAxesPaths() { + this.timeSeries = createTimeSeries(this.graphData.queries[0].result, + this.graphWidth, + this.graphHeight, + this.graphHeightOffset); + + if (this.timeSeries.length > 3) { + this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20; + } + const axisXScale = d3.time.scale() .range([0, this.graphWidth]); - this.yScale = d3.scale.linear() + const axisYScale = d3.scale.linear() .range([this.graphHeight - this.graphHeightOffset, 0]); - axisXScale.domain(d3.extent(this.data, d => d.time)); - this.yScale.domain([0, d3.max(this.data.map(d => d.value))]); + + axisXScale.domain(d3.extent(this.timeSeries[0].values, d => d.time)); + axisYScale.domain([0, d3.max(this.timeSeries[0].values.map(d => d.value))]); const xAxis = d3.svg.axis() .scale(axisXScale) @@ -164,7 +167,7 @@ .orient('bottom'); const yAxis = d3.svg.axis() - .scale(this.yScale) + .scale(axisYScale) .ticks(measurements.yTicks) .orient('left'); @@ -180,25 +183,6 @@ .attr('class', 'axis-tick'); } // Avoid adding the class to the first tick, to prevent coloring }); // This will select all of the ticks once they're rendered - - this.xScale = d3.time.scale() - .range([0, this.graphWidth - 70]); - - this.xScale.domain(d3.extent(this.data, d => d.time)); - - const areaFunction = d3.svg.area() - .x(d => this.xScale(d.time)) - .y0(this.graphHeight - this.graphHeightOffset) - .y1(d => this.yScale(d.value)) - .interpolate('linear'); - - const lineFunction = d3.svg.line() - .x(d => this.xScale(d.time)) - .y(d => this.yScale(d.value)); - - this.line = lineFunction(this.data); - - this.area = areaFunction(this.data); }, }, @@ -219,12 +203,11 @@ }, }; </script> + <template> - <div - :class="classType"> - <h5 - class="text-center graph-title"> - {{graphData.title}} + <div class="prometheus-graph"> + <h5 class="text-center graph-title"> + {{graphData.title}} </h5> <div class="prometheus-svg-container" @@ -245,30 +228,25 @@ :graph-height="graphHeight" :margin="margin" :measurements="measurements" - :area-color-rgb="areaColorRgb" :legend-title="legendTitle" :y-axis-label="yAxisLabel" - :metric-usage="metricUsage" + :time-series="timeSeries" + :unit-of-display="unitOfDisplay" + :current-data-index="currentDataIndex" /> <svg class="graph-data" :viewBox="innerViewBox" ref="graphData"> - <path - class="metric-area" - :d="area" - :fill="areaColorRgb" - transform="translate(-5, 20)"> - </path> - <path - class="metric-line" - :d="line" - :stroke="lineColorRgb" - fill="none" - stroke-width="2" - transform="translate(-5, 20)"> - </path> - <graph-deployment + <monitoring-paths + v-for="(path, index) in timeSeries" + :key="index" + :generated-line-path="path.linePath" + :generated-area-path="path.areaPath" + :line-color="path.lineColor" + :area-color="path.areaColor" + /> + <monitoring-deployment :show-deploy-info="showDeployInfo" :deployment-data="reducedDeploymentData" :graph-height="graphHeight" @@ -277,7 +255,6 @@ <graph-flag v-if="showFlag" :current-x-coordinate="currentXCoordinate" - :current-y-coordinate="currentYCoordinate" :current-data="currentData" :current-flag-position="currentFlagPosition" :graph-height="graphHeight" diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue index c4d4647d240..a98e3d06c18 100644 --- a/app/assets/javascripts/monitoring/components/graph/flag.vue +++ b/app/assets/javascripts/monitoring/components/graph/flag.vue @@ -7,10 +7,6 @@ type: Number, required: true, }, - currentYCoordinate: { - type: Number, - required: true, - }, currentFlagPosition: { type: Number, required: true, @@ -60,16 +56,7 @@ :y2="calculatedHeight" transform="translate(-5, 20)"> </line> - <circle - class="circle-metric" - :fill="circleColorRgb" - stroke="#000" - :cx="currentXCoordinate" - :cy="currentYCoordinate" - r="5" - transform="translate(-5, 20)"> - </circle> - <svg + <svg class="rect-text-metric" :x="currentFlagPosition" y="0"> diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue index d08f9cbffd4..a43dad8e601 100644 --- a/app/assets/javascripts/monitoring/components/graph/legend.vue +++ b/app/assets/javascripts/monitoring/components/graph/legend.vue @@ -1,4 +1,6 @@ <script> + import { formatRelevantDigits } from '../../../lib/utils/number_utils'; + export default { props: { graphWidth: { @@ -17,10 +19,6 @@ type: Object, required: true, }, - areaColorRgb: { - type: String, - required: true, - }, legendTitle: { type: String, required: true, @@ -29,15 +27,25 @@ type: String, required: true, }, - metricUsage: { + timeSeries: { + type: Array, + required: true, + }, + unitOfDisplay: { type: String, required: true, }, + currentDataIndex: { + type: Number, + required: true, + }, }, data() { return { yLabelWidth: 0, yLabelHeight: 0, + seriesXPosition: 0, + metricUsageXPosition: 0, }; }, computed: { @@ -63,10 +71,28 @@ yPosition() { return ((this.graphHeight - this.margin.top) + this.measurements.axisLabelLineOffset) || 0; }, + + }, + methods: { + translateLegendGroup(index) { + return `translate(0, ${12 * (index)})`; + }, + + formatMetricUsage(series) { + return `${formatRelevantDigits(series.values[this.currentDataIndex].value)} ${this.unitOfDisplay}`; + }, }, mounted() { this.$nextTick(() => { const bbox = this.$refs.ylabel.getBBox(); + this.metricUsageXPosition = 0; + this.seriesXPosition = 0; + if (this.$refs.legendTitleSvg != null) { + this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width; + } + if (this.$refs.seriesTitleSvg != null) { + this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width; + } this.yLabelWidth = bbox.width + 10; // Added some padding this.yLabelHeight = bbox.height + 5; }); @@ -121,24 +147,33 @@ dy=".35em"> Time </text> - <rect - :fill="areaColorRgb" - :width="measurements.legends.width" - :height="measurements.legends.height" - x="20" - :y="graphHeight - measurements.legendOffset"> - </rect> - <text - class="text-metric-title" - x="50" - :y="graphHeight - 25"> - {{legendTitle}} - </text> - <text - class="text-metric-usage" - x="50" - :y="graphHeight - 10"> - {{metricUsage}} - </text> + <g class="legend-group" + v-for="(series, index) in timeSeries" + :key="index" + :transform="translateLegendGroup(index)"> + <rect + :fill="series.areaColor" + :width="measurements.legends.width" + :height="measurements.legends.height" + x="20" + :y="graphHeight - measurements.legendOffset"> + </rect> + <text + v-if="timeSeries.length > 1" + class="legend-metric-title" + ref="legendTitleSvg" + x="38" + :y="graphHeight - 30"> + {{legendTitle}} Series {{index + 1}} {{formatMetricUsage(series)}} + </text> + <text + v-else + class="legend-metric-title" + ref="legendTitleSvg" + x="38" + :y="graphHeight - 30"> + {{legendTitle}} {{formatMetricUsage(series)}} + </text> + </g> </g> </template> diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue index 32c90fda8cc..958f537d31b 100644 --- a/app/assets/javascripts/monitoring/components/graph_group.vue +++ b/app/assets/javascripts/monitoring/components/graph_group.vue @@ -14,7 +14,7 @@ export default { <div class="panel-heading"> <h4>{{name}}</h4> </div> - <div class="panel-body"> + <div class="panel-body prometheus-graph-group"> <slot /> </div> </div> diff --git a/app/assets/javascripts/monitoring/components/graph_row.vue b/app/assets/javascripts/monitoring/components/graph_row.vue deleted file mode 100644 index bdb9149c3b4..00000000000 --- a/app/assets/javascripts/monitoring/components/graph_row.vue +++ /dev/null @@ -1,41 +0,0 @@ -<script> - import Graph from './graph.vue'; - - export default { - props: { - rowData: { - type: Array, - required: true, - }, - updateAspectRatio: { - type: Boolean, - required: true, - }, - deploymentData: { - type: Array, - required: true, - }, - }, - components: { - Graph, - }, - computed: { - bootstrapClass() { - return this.rowData.length >= 2 ? 'col-md-6' : 'col-md-12'; - }, - }, - }; -</script> - -<template> - <div class="prometheus-row row"> - <graph - v-for="(graphData, index) in rowData" - :graph-data="graphData" - :class-type="bootstrapClass" - :key="index" - :update-aspect-ratio="updateAspectRatio" - :deployment-data="deploymentData" - /> - </div> -</template> diff --git a/app/assets/javascripts/monitoring/components/monitoring_paths.vue b/app/assets/javascripts/monitoring/components/monitoring_paths.vue new file mode 100644 index 00000000000..043f1bf66bb --- /dev/null +++ b/app/assets/javascripts/monitoring/components/monitoring_paths.vue @@ -0,0 +1,40 @@ +<script> + export default { + props: { + generatedLinePath: { + type: String, + required: true, + }, + generatedAreaPath: { + type: String, + required: true, + }, + lineColor: { + type: String, + required: true, + }, + areaColor: { + type: String, + required: true, + }, + }, + }; +</script> +<template> + <g> + <path + class="metric-area" + :d="generatedAreaPath" + :fill="areaColor" + transform="translate(-5, 20)"> + </path> + <path + class="metric-line" + :d="generatedLinePath" + :stroke="lineColor" + fill="none" + stroke-width="1" + transform="translate(-5, 20)"> + </path> + </g> +</template> diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js index 8e62fa63f13..345a0b37a76 100644 --- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js +++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js @@ -21,9 +21,9 @@ const mixins = { formatDeployments() { this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => { const time = new Date(deployment.created_at); - const xPos = Math.floor(this.xScale(time)); + const xPos = Math.floor(this.timeSeries[0].timeSeriesScaleX(time)); - time.setSeconds(this.data[0].time.getSeconds()); + time.setSeconds(this.timeSeries[0].values[0].time.getSeconds()); if (xPos >= 0) { deploymentDataArray.push({ diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js index 737c964f12e..7592af5878e 100644 --- a/app/assets/javascripts/monitoring/stores/monitoring_store.js +++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js @@ -1,46 +1,36 @@ import _ from 'underscore'; -class MonitoringStore { +function sortMetrics(metrics) { + return _.chain(metrics).sortBy('weight').sortBy('title').value(); +} + +function normalizeMetrics(metrics) { + return metrics.map(metric => ({ + ...metric, + queries: metric.queries.map(query => ({ + ...query, + result: query.result.map(result => ({ + ...result, + values: result.values.map(([timestamp, value]) => ({ + time: new Date(timestamp * 1000), + value, + })), + })), + })), + })); +} + +export default class MonitoringStore { constructor() { this.groups = []; this.deploymentData = []; } - // eslint-disable-next-line class-methods-use-this - createArrayRows(metrics = []) { - const currentMetrics = metrics; - const availableMetrics = []; - let metricsRow = []; - let index = 1; - Object.keys(currentMetrics).forEach((key) => { - const metricValues = currentMetrics[key].queries[0].result[0].values; - if (metricValues != null) { - const literalMetrics = metricValues.map(metric => ({ - time: new Date(metric[0] * 1000), - value: metric[1], - })); - currentMetrics[key].queries[0].result[0].values = literalMetrics; - metricsRow.push(currentMetrics[key]); - if (index % 2 === 0) { - availableMetrics.push(metricsRow); - metricsRow = []; - } - index = index += 1; - } - }); - if (metricsRow.length > 0) { - availableMetrics.push(metricsRow); - } - return availableMetrics; - } - storeMetrics(groups = []) { - this.groups = groups.map((group) => { - const currentGroup = group; - currentGroup.metrics = _.chain(group.metrics).sortBy('weight').sortBy('title').value(); - currentGroup.metrics = this.createArrayRows(currentGroup.metrics); - return currentGroup; - }); + this.groups = groups.map(group => ({ + ...group, + metrics: normalizeMetrics(sortMetrics(group.metrics)), + })); } storeDeploymentData(deploymentData = []) { @@ -48,14 +38,6 @@ class MonitoringStore { } getMetricsCount() { - let metricsCount = 0; - this.groups.forEach((group) => { - group.metrics.forEach((metric) => { - metricsCount = metricsCount += metric.length; - }); - }); - return metricsCount; + return this.groups.reduce((count, group) => count + group.metrics.length, 0); } } - -export default MonitoringStore; diff --git a/app/assets/javascripts/monitoring/utils/measurements.js b/app/assets/javascripts/monitoring/utils/measurements.js index 62cd19c86e1..ee3c45efacc 100644 --- a/app/assets/javascripts/monitoring/utils/measurements.js +++ b/app/assets/javascripts/monitoring/utils/measurements.js @@ -7,15 +7,15 @@ export default { left: 40, }, legends: { - width: 15, - height: 25, + width: 10, + height: 3, }, backgroundLegend: { width: 30, height: 50, }, axisLabelLineOffset: -20, - legendOffset: 35, + legendOffset: 33, }, large: { // This covers both md and lg screen sizes margin: { @@ -25,15 +25,15 @@ export default { left: 80, }, legends: { - width: 20, - height: 30, + width: 15, + height: 3, }, backgroundLegend: { width: 30, height: 150, }, axisLabelLineOffset: 20, - legendOffset: 38, + legendOffset: 36, }, xTicks: 8, yTicks: 3, diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js new file mode 100644 index 00000000000..05d551e917c --- /dev/null +++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js @@ -0,0 +1,80 @@ +import d3 from 'd3'; +import _ from 'underscore'; + +export default function createTimeSeries(seriesData, graphWidth, graphHeight, graphHeightOffset) { + const maxValues = seriesData.map((timeSeries, index) => { + const maxValue = d3.max(timeSeries.values.map(d => d.value)); + return { + maxValue, + index, + }; + }); + + const maxValueFromSeries = _.max(maxValues, val => val.maxValue); + + let timeSeriesNumber = 1; + let lineColor = '#1f78d1'; + let areaColor = '#8fbce8'; + return seriesData.map((timeSeries) => { + const timeSeriesScaleX = d3.time.scale() + .range([0, graphWidth - 70]); + + const timeSeriesScaleY = d3.scale.linear() + .range([graphHeight - graphHeightOffset, 0]); + + timeSeriesScaleX.domain(d3.extent(timeSeries.values, d => d.time)); + timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]); + + const lineFunction = d3.svg.line() + .x(d => timeSeriesScaleX(d.time)) + .y(d => timeSeriesScaleY(d.value)); + + const areaFunction = d3.svg.area() + .x(d => timeSeriesScaleX(d.time)) + .y0(graphHeight - graphHeightOffset) + .y1(d => timeSeriesScaleY(d.value)) + .interpolate('linear'); + + switch (timeSeriesNumber) { + case 1: + lineColor = '#1f78d1'; + areaColor = '#8fbce8'; + break; + case 2: + lineColor = '#fc9403'; + areaColor = '#feca81'; + break; + case 3: + lineColor = '#db3b21'; + areaColor = '#ed9d90'; + break; + case 4: + lineColor = '#1aaa55'; + areaColor = '#8dd5aa'; + break; + case 5: + lineColor = '#6666c4'; + areaColor = '#d1d1f0'; + break; + default: + lineColor = '#1f78d1'; + areaColor = '#8fbce8'; + break; + } + + if (timeSeriesNumber <= 5) { + timeSeriesNumber = timeSeriesNumber += 1; + } else { + timeSeriesNumber = 1; + } + + return { + linePath: lineFunction(timeSeries.values), + areaPath: areaFunction(timeSeries.values), + timeSeriesScaleX, + values: timeSeries.values, + lineColor, + areaColor, + }; + }); +} diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js index 05e3f33f5ed..709a5d33b9f 100644 --- a/app/assets/javascripts/new_sidebar.js +++ b/app/assets/javascripts/new_sidebar.js @@ -63,7 +63,7 @@ export default class NewNavSidebar { if (breakpoint === 'sm' || breakpoint === 'md') { this.toggleCollapsedSidebar(true); } else if (breakpoint === 'lg') { - const collapse = Cookies.get('sidebar_collapsed') === 'true'; + const collapse = this.$sidebar.hasClass('sidebar-icons-only'); this.toggleCollapsedSidebar(collapse); } } diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index b38a6abc8d1..a09270d6d24 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -464,7 +464,6 @@ export default class Notes { } renderDiscussionAvatar(diffAvatarContainer, noteEntity) { - var commentButton = diffAvatarContainer.find('.js-add-diff-note-button'); var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders'); if (!avatarHolder.length) { @@ -475,10 +474,6 @@ export default class Notes { gl.diffNotesCompileComponents(); } - - if (commentButton.length) { - commentButton.remove(); - } } /** @@ -767,6 +762,7 @@ export default class Notes { var $note, $notes; $note = $(el); $notes = $note.closest('.discussion-notes'); + const discussionId = $('.notes', $notes).data('discussion-id'); if (typeof gl.diffNotesCompileComponents !== 'undefined') { if (gl.diffNoteApps[noteElId]) { @@ -783,6 +779,8 @@ export default class Notes { // "Discussions" tab $notes.closest('.timeline-entry').remove(); + $(`.js-diff-avatars-${discussionId}`).trigger('remove.vue'); + // The notes tr can contain multiple lists of notes, like on the parallel diff if (notesTr.find('.discussion-notes').length > 1) { $notes.remove(); diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index d7e3ab42f00..fe6602259e2 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -53,10 +53,6 @@ import Cookies from 'js-cookie'; return _this.changeProject($(e.currentTarget).val()); }; })(this)); - return $('.js-projects-dropdown-toggle').on('click', function(e) { - e.preventDefault(); - return $('.js-projects-dropdown').select2('open'); - }); }; Project.prototype.changeProject = function(url) { diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 1b4ed6be90a..fb01390f91c 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -5,48 +5,6 @@ import ProjectSelectComboButton from './project_select_combo_button'; (function() { this.ProjectSelect = (function() { function ProjectSelect() { - $('.js-projects-dropdown-toggle').each(function(i, dropdown) { - var $dropdown; - $dropdown = $(dropdown); - return $dropdown.glDropdown({ - filterable: true, - filterRemote: true, - search: { - fields: ['name_with_namespace'] - }, - data: function(term, callback) { - var finalCallback, projectsCallback; - var orderBy = $dropdown.data('order-by'); - finalCallback = function(projects) { - return callback(projects); - }; - if (this.includeGroups) { - projectsCallback = function(projects) { - var groupsCallback; - groupsCallback = function(groups) { - var data; - data = groups.concat(projects); - return finalCallback(data); - }; - return Api.groups(term, {}, groupsCallback); - }; - } else { - projectsCallback = finalCallback; - } - if (this.groupId) { - return Api.groupProjects(this.groupId, term, projectsCallback); - } else { - return Api.projects(term, { order_by: orderBy }, projectsCallback); - } - }, - url: function(project) { - return project.web_url; - }, - text: function(project) { - return project.name_with_namespace; - } - }); - }); $('.ajax-project-select').each(function(i, select) { var placeholder; this.groupId = $(select).data('group-id'); diff --git a/app/assets/javascripts/projects_dropdown/components/app.vue b/app/assets/javascripts/projects_dropdown/components/app.vue new file mode 100644 index 00000000000..7606605be32 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/components/app.vue @@ -0,0 +1,157 @@ +<script> +import bs from '../../breakpoints'; +import eventHub from '../event_hub'; +import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + +import projectsListFrequent from './projects_list_frequent.vue'; +import projectsListSearch from './projects_list_search.vue'; + +import search from './search.vue'; + +export default { + components: { + search, + loadingIcon, + projectsListFrequent, + projectsListSearch, + }, + props: { + currentProject: { + type: Object, + required: true, + }, + store: { + type: Object, + required: true, + }, + service: { + type: Object, + required: true, + }, + }, + data() { + return { + isLoadingProjects: false, + isFrequentsListVisible: false, + isSearchListVisible: false, + isLocalStorageFailed: false, + isSearchFailed: false, + searchQuery: '', + }; + }, + computed: { + frequentProjects() { + return this.store.getFrequentProjects(); + }, + searchProjects() { + return this.store.getSearchedProjects(); + }, + }, + methods: { + toggleFrequentProjectsList(state) { + this.isLoadingProjects = !state; + this.isSearchListVisible = !state; + this.isFrequentsListVisible = state; + }, + toggleSearchProjectsList(state) { + this.isLoadingProjects = !state; + this.isFrequentsListVisible = !state; + this.isSearchListVisible = state; + }, + toggleLoader(state) { + this.isFrequentsListVisible = !state; + this.isSearchListVisible = !state; + this.isLoadingProjects = state; + }, + fetchFrequentProjects() { + const screenSize = bs.getBreakpointSize(); + if (this.searchQuery && (screenSize !== 'sm' && screenSize !== 'xs')) { + this.toggleSearchProjectsList(true); + } else { + this.toggleLoader(true); + this.isLocalStorageFailed = false; + const projects = this.service.getFrequentProjects(); + if (projects) { + this.toggleFrequentProjectsList(true); + this.store.setFrequentProjects(projects); + } else { + this.isLocalStorageFailed = true; + this.toggleFrequentProjectsList(true); + this.store.setFrequentProjects([]); + } + } + }, + fetchSearchedProjects(searchQuery) { + this.searchQuery = searchQuery; + this.toggleLoader(true); + this.service.getSearchedProjects(this.searchQuery) + .then(res => res.json()) + .then((results) => { + this.toggleSearchProjectsList(true); + this.store.setSearchedProjects(results); + }) + .catch(() => { + this.isSearchFailed = true; + this.toggleSearchProjectsList(true); + }); + }, + logCurrentProjectAccess() { + this.service.logProjectAccess(this.currentProject); + }, + handleSearchClear() { + this.searchQuery = ''; + this.toggleFrequentProjectsList(true); + this.store.clearSearchedProjects(); + }, + handleSearchFailure() { + this.isSearchFailed = true; + this.toggleSearchProjectsList(true); + }, + }, + created() { + if (this.currentProject.id) { + this.logCurrentProjectAccess(); + } + + eventHub.$on('dropdownOpen', this.fetchFrequentProjects); + eventHub.$on('searchProjects', this.fetchSearchedProjects); + eventHub.$on('searchCleared', this.handleSearchClear); + eventHub.$on('searchFailed', this.handleSearchFailure); + }, + beforeDestroy() { + eventHub.$off('dropdownOpen', this.fetchFrequentProjects); + eventHub.$off('searchProjects', this.fetchSearchedProjects); + eventHub.$off('searchCleared', this.handleSearchClear); + eventHub.$off('searchFailed', this.handleSearchFailure); + }, +}; +</script> + +<template> + <div> + <search/> + <loading-icon + class="loading-animation prepend-top-20" + size="2" + v-if="isLoadingProjects" + :label="s__('ProjectsDropdown|Loading projects')" + /> + <div + class="section-header" + v-if="isFrequentsListVisible" + > + {{ s__('ProjectsDropdown|Frequently visited') }} + </div> + <projects-list-frequent + v-if="isFrequentsListVisible" + :local-storage-failed="isLocalStorageFailed" + :projects="frequentProjects" + /> + <projects-list-search + v-if="isSearchListVisible" + :search-failed="isSearchFailed" + :matcher="searchQuery" + :projects="searchProjects" + /> + </div> +</template> diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue new file mode 100644 index 00000000000..093554cd0bc --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue @@ -0,0 +1,57 @@ +<script> +import { s__ } from '../../locale'; +import projectsListItem from './projects_list_item.vue'; + +export default { + components: { + projectsListItem, + }, + props: { + projects: { + type: Array, + required: true, + }, + localStorageFailed: { + type: Boolean, + required: true, + }, + }, + computed: { + isListEmpty() { + return this.projects.length === 0; + }, + listEmptyMessage() { + return this.localStorageFailed ? + s__('ProjectsDropdown|This feature requires browser localStorage support') : + s__('ProjectsDropdown|Projects you visit often will appear here'); + }, + }, +}; +</script> + +<template> + <div + class="projects-list-frequent-container" + > + <ul + class="list-unstyled" + > + <li + class="section-empty" + v-if="isListEmpty" + > + {{listEmptyMessage}} + </li> + <projects-list-item + v-else + v-for="(project, index) in projects" + :key="index" + :project-id="project.id" + :project-name="project.name" + :namespace="project.namespace" + :web-url="project.webUrl" + :avatar-url="project.avatarUrl" + /> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue new file mode 100644 index 00000000000..fe5179de206 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue @@ -0,0 +1,96 @@ +<script> +import identicon from '../../vue_shared/components/identicon.vue'; + +export default { + components: { + identicon, + }, + props: { + matcher: { + type: String, + required: false, + }, + projectId: { + type: Number, + required: true, + }, + projectName: { + type: String, + required: true, + }, + namespace: { + type: String, + required: true, + }, + webUrl: { + type: String, + required: true, + }, + avatarUrl: { + required: true, + validator(value) { + return value === null || typeof value === 'string'; + }, + }, + }, + computed: { + hasAvatar() { + return this.avatarUrl !== null; + }, + highlightedProjectName() { + if (this.matcher) { + const matcherRegEx = new RegExp(this.matcher, 'gi'); + const matches = this.projectName.match(matcherRegEx); + + if (matches && matches.length > 0) { + return this.projectName.replace(matches[0], `<b>${matches[0]}</b>`); + } + } + return this.projectName; + }, + }, +}; +</script> + +<template> + <li + class="projects-list-item-container" + > + <a + class="clearfix" + :href="webUrl" + > + <div + class="project-item-avatar-container" + > + <img + v-if="hasAvatar" + class="avatar s32" + :src="avatarUrl" + /> + <identicon + v-else + size-class="s32" + :entity-id=projectId + :entity-name="projectName" + /> + </div> + <div + class="project-item-metadata-container" + > + <div + class="project-title" + :title="projectName" + v-html="highlightedProjectName" + > + </div> + <div + class="project-namespace" + :title="namespace" + > + {{namespace}} + </div> + </div> + </a> + </li> +</template> diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue new file mode 100644 index 00000000000..fa5efef2919 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue @@ -0,0 +1,63 @@ +<script> +import { s__ } from '../../locale'; +import projectsListItem from './projects_list_item.vue'; + +export default { + components: { + projectsListItem, + }, + props: { + matcher: { + type: String, + required: true, + }, + projects: { + type: Array, + required: true, + }, + searchFailed: { + type: Boolean, + required: true, + }, + }, + computed: { + isListEmpty() { + return this.projects.length === 0; + }, + listEmptyMessage() { + return this.searchFailed ? + s__('ProjectsDropdown|Something went wrong on our end.') : + s__('ProjectsDropdown|No projects matched your query'); + }, + }, +}; +</script> + +<template> + <div + class="projects-list-search-container" + > + <ul + class="list-unstyled" + > + <li + v-if="isListEmpty" + :class="{ 'section-failure': searchFailed }" + class="section-empty" + > + {{ listEmptyMessage }} + </li> + <projects-list-item + v-else + v-for="(project, index) in projects" + :key="index" + :project-id="project.id" + :project-name="project.name" + :namespace="project.namespace" + :web-url="project.webUrl" + :avatar-url="project.avatarUrl" + :matcher="matcher" + /> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/projects_dropdown/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue new file mode 100644 index 00000000000..b71997234e5 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/components/search.vue @@ -0,0 +1,64 @@ +<script> +import _ from 'underscore'; +import eventHub from '../event_hub'; + +export default { + data() { + return { + searchQuery: '', + }; + }, + watch: { + searchQuery() { + this.handleInput(); + }, + }, + methods: { + setFocus() { + this.$refs.search.focus(); + }, + emitSearchEvents() { + if (this.searchQuery) { + eventHub.$emit('searchProjects', this.searchQuery); + } else { + eventHub.$emit('searchCleared'); + } + }, + /** + * Callback function within _.debounce is intentionally + * kept as ES5 `function() {}` instead of ES6 `() => {}` + * as it otherwise messes up function context + * and component reference is no longer accessible via `this` + */ + // eslint-disable-next-line func-names + handleInput: _.debounce(function () { + this.emitSearchEvents(); + }, 500), + }, + mounted() { + eventHub.$on('dropdownOpen', this.setFocus); + }, + beforeDestroy() { + eventHub.$off('dropdownOpen', this.setFocus); + }, +}; +</script> + +<template> + <div + class="search-input-container hidden-xs" + > + <input + type="search" + class="form-control" + ref="search" + v-model="searchQuery" + :placeholder="s__('ProjectsDropdown|Search projects')" + /> + <i + v-if="!searchQuery" + class="search-icon fa fa-fw fa-search" + aria-hidden="true" + /> + </div> +</template> diff --git a/app/assets/javascripts/projects_dropdown/constants.js b/app/assets/javascripts/projects_dropdown/constants.js new file mode 100644 index 00000000000..8937097184c --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/constants.js @@ -0,0 +1,10 @@ +export const FREQUENT_PROJECTS = { + MAX_COUNT: 20, + LIST_COUNT_DESKTOP: 5, + LIST_COUNT_MOBILE: 3, + ELIGIBLE_FREQUENCY: 3, +}; + +export const HOUR_IN_MS = 3600000; + +export const STORAGE_KEY = 'frequent-projects'; diff --git a/app/assets/javascripts/projects_dropdown/event_hub.js b/app/assets/javascripts/projects_dropdown/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/projects_dropdown/index.js b/app/assets/javascripts/projects_dropdown/index.js new file mode 100644 index 00000000000..2660da3c558 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/index.js @@ -0,0 +1,68 @@ +import Vue from 'vue'; + +import Translate from '../vue_shared/translate'; +import eventHub from './event_hub'; +import ProjectsService from './service/projects_service'; +import ProjectsStore from './store/projects_store'; + +import projectsDropdownApp from './components/app.vue'; + +Vue.use(Translate); + +document.addEventListener('DOMContentLoaded', () => { + const el = document.getElementById('js-projects-dropdown'); + const navEl = document.getElementById('nav-projects-dropdown'); + + // Don't do anything if element doesn't exist (No projects dropdown) + // This is for when the user accesses GitLab without logging in + if (!el || !navEl) { + return; + } + + $(navEl).on('show.bs.dropdown', (e) => { + const dropdownEl = $(e.currentTarget).find('.projects-dropdown-menu'); + dropdownEl.one('transitionend', () => { + eventHub.$emit('dropdownOpen'); + }); + }); + + // eslint-disable-next-line no-new + new Vue({ + el, + components: { + projectsDropdownApp, + }, + data() { + const dataset = this.$options.el.dataset; + const store = new ProjectsStore(); + const service = new ProjectsService(dataset.userName); + + const project = { + id: Number(dataset.projectId), + name: dataset.projectName, + namespace: dataset.projectNamespace, + webUrl: dataset.projectWebUrl, + avatarUrl: dataset.projectAvatarUrl || null, + lastAccessedOn: Date.now(), + }; + + return { + store, + service, + state: store.state, + currentUserName: dataset.userName, + currentProject: project, + }; + }, + render(createElement) { + return createElement('projects-dropdown-app', { + props: { + currentUserName: this.currentUserName, + currentProject: this.currentProject, + store: this.store, + service: this.service, + }, + }); + }, + }); +}); diff --git a/app/assets/javascripts/projects_dropdown/service/projects_service.js b/app/assets/javascripts/projects_dropdown/service/projects_service.js new file mode 100644 index 00000000000..fad956b4c26 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/service/projects_service.js @@ -0,0 +1,132 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +import bp from '../../breakpoints'; +import Api from '../../api'; +import AccessorUtilities from '../../lib/utils/accessor'; + +import { FREQUENT_PROJECTS, HOUR_IN_MS, STORAGE_KEY } from '../constants'; + +Vue.use(VueResource); + +export default class ProjectsService { + constructor(currentUserName) { + this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); + this.currentUserName = currentUserName; + this.storageKey = `${this.currentUserName}/${STORAGE_KEY}`; + this.projectsPath = Vue.resource(Api.buildUrl(Api.projectsPath)); + } + + getSearchedProjects(searchQuery) { + return this.projectsPath.get({ + simple: false, + per_page: 20, + membership: !!gon.current_user_id, + order_by: 'last_activity_at', + search: searchQuery, + }); + } + + getFrequentProjects() { + if (this.isLocalStorageAvailable) { + return this.getTopFrequentProjects(); + } + return null; + } + + logProjectAccess(project) { + let matchFound = false; + let storedFrequentProjects; + + if (this.isLocalStorageAvailable) { + const storedRawProjects = localStorage.getItem(this.storageKey); + + // Check if there's any frequent projects list set + if (!storedRawProjects) { + // No frequent projects list set, set one up. + storedFrequentProjects = []; + storedFrequentProjects.push({ ...project, frequency: 1 }); + } else { + // Check if project is already present in frequents list + // When found, update metadata of it. + storedFrequentProjects = JSON.parse(storedRawProjects).map((projectItem) => { + if (projectItem.id === project.id) { + matchFound = true; + const diff = Math.abs(project.lastAccessedOn - projectItem.lastAccessedOn) / HOUR_IN_MS; + const updatedProject = { + ...project, + frequency: projectItem.frequency, + lastAccessedOn: projectItem.lastAccessedOn, + }; + + // Check if duration since last access of this project + // is over an hour + if (diff > 1) { + return { + ...updatedProject, + frequency: updatedProject.frequency + 1, + lastAccessedOn: Date.now(), + }; + } + + return { + ...updatedProject, + }; + } + + return projectItem; + }); + + // Check whether currently logged project is present in frequents list + if (!matchFound) { + // We always keep size of frequents collection to 20 projects + // out of which only 5 projects with + // highest value of `frequency` and most recent `lastAccessedOn` + // are shown in projects dropdown + if (storedFrequentProjects.length === FREQUENT_PROJECTS.MAX_COUNT) { + storedFrequentProjects.shift(); // Remove an item from head of array + } + + storedFrequentProjects.push({ ...project, frequency: 1 }); + } + } + + localStorage.setItem(this.storageKey, JSON.stringify(storedFrequentProjects)); + } + } + + getTopFrequentProjects() { + const storedFrequentProjects = JSON.parse(localStorage.getItem(this.storageKey)); + let frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_DESKTOP; + + if (!storedFrequentProjects) { + return []; + } + + if (bp.getBreakpointSize() === 'sm' || + bp.getBreakpointSize() === 'xs') { + frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_MOBILE; + } + + const frequentProjects = storedFrequentProjects + .filter(project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY); + + // Sort all frequent projects in decending order of frequency + // and then by lastAccessedOn with recent most first + frequentProjects.sort((projectA, projectB) => { + if (projectA.frequency < projectB.frequency) { + return 1; + } else if (projectA.frequency > projectB.frequency) { + return -1; + } else if (projectA.lastAccessedOn < projectB.lastAccessedOn) { + return 1; + } else if (projectA.lastAccessedOn > projectB.lastAccessedOn) { + return -1; + } + + return 0; + }); + + return _.first(frequentProjects, frequentProjectsCount); + } +} diff --git a/app/assets/javascripts/projects_dropdown/store/projects_store.js b/app/assets/javascripts/projects_dropdown/store/projects_store.js new file mode 100644 index 00000000000..ffefbe693f4 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/store/projects_store.js @@ -0,0 +1,33 @@ +export default class ProjectsStore { + constructor() { + this.state = {}; + this.state.frequentProjects = []; + this.state.searchedProjects = []; + } + + setFrequentProjects(rawProjects) { + this.state.frequentProjects = rawProjects; + } + + getFrequentProjects() { + return this.state.frequentProjects; + } + + setSearchedProjects(rawProjects) { + this.state.searchedProjects = rawProjects.map(rawProject => ({ + id: rawProject.id, + name: rawProject.name, + namespace: rawProject.name_with_namespace, + webUrl: rawProject.web_url, + avatarUrl: rawProject.avatar_url, + })); + } + + getSearchedProjects() { + return this.state.searchedProjects; + } + + clearSearchedProjects() { + this.state.searchedProjects = []; + } +} diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index fa958d75fa4..a4eae135403 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -2,7 +2,6 @@ import _ from 'underscore'; import Cookies from 'js-cookie'; -import SidebarHeightManager from './sidebar_height_manager'; (function() { this.Sidebar = (function() { @@ -23,7 +22,6 @@ import SidebarHeightManager from './sidebar_height_manager'; }; Sidebar.prototype.addEventListeners = function() { - SidebarHeightManager.init(); const $document = $(document); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); @@ -157,11 +155,16 @@ import SidebarHeightManager from './sidebar_height_manager'; Sidebar.prototype.openDropdown = function(blockOrName) { var $block; $block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName; - $block.find('.edit-link').trigger('click'); if (!this.isOpen()) { this.setCollapseAfterUpdate($block); - return this.toggleSidebar('open'); + this.toggleSidebar('open'); } + + // Wait for the sidebar to trigger('click') open + // so it doesn't cause our dropdown to close preemptively + setTimeout(() => { + $block.find('.js-sidebar-dropdown-toggle').trigger('click'); + }); }; Sidebar.prototype.setCollapseAfterUpdate = function($block) { diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.js b/app/assets/javascripts/sidebar/components/assignees/assignee_title.js index 5a6e47e566e..77f070d48cc 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.js +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.js @@ -36,7 +36,7 @@ export default { /> <a v-if="editable" - class="edit-link pull-right" + class="js-sidebar-dropdown-toggle edit-link pull-right" href="#" > Edit diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js new file mode 100644 index 00000000000..1c15a1b877a --- /dev/null +++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js @@ -0,0 +1,85 @@ +/* global Flash */ + +function isValidProjectId(id) { + return id > 0; +} + +class SidebarMoveIssue { + constructor(mediator, dropdownToggle, confirmButton) { + this.mediator = mediator; + + this.$dropdownToggle = $(dropdownToggle); + this.$confirmButton = $(confirmButton); + + this.onConfirmClickedWrapper = this.onConfirmClicked.bind(this); + } + + init() { + this.initDropdown(); + this.addEventListeners(); + } + + destroy() { + this.removeEventListeners(); + } + + initDropdown() { + this.$dropdownToggle.glDropdown({ + search: { + fields: ['name_with_namespace'], + }, + showMenuAbove: true, + selectable: true, + filterable: true, + filterRemote: true, + multiSelect: false, + // Keep the dropdown open after selecting an option + shouldPropagate: false, + data: (searchTerm, callback) => { + this.mediator.fetchAutocompleteProjects(searchTerm) + .then(callback) + .catch(() => new Flash('An error occured while fetching projects autocomplete.')); + }, + renderRow: project => ` + <li> + <a href="#" class="js-move-issue-dropdown-item"> + ${project.name_with_namespace} + </a> + </li> + `, + clicked: (options) => { + const project = options.selectedObj; + const selectedProjectId = options.isMarking ? project.id : 0; + this.mediator.setMoveToProjectId(selectedProjectId); + + this.$confirmButton.attr('disabled', !isValidProjectId(selectedProjectId)); + }, + }); + } + + addEventListeners() { + this.$confirmButton.on('click', this.onConfirmClickedWrapper); + } + + removeEventListeners() { + this.$confirmButton.off('click', this.onConfirmClickedWrapper); + } + + onConfirmClicked() { + if (isValidProjectId(this.mediator.store.moveToProjectId)) { + this.$confirmButton + .disable() + .addClass('is-loading'); + + this.mediator.moveIssue() + .catch(() => { + Flash('An error occured while moving the issue.'); + this.$confirmButton + .enable() + .removeClass('is-loading'); + }); + } + } +} + +export default SidebarMoveIssue; diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js index 5a82d01dc41..604648407a4 100644 --- a/app/assets/javascripts/sidebar/services/sidebar_service.js +++ b/app/assets/javascripts/sidebar/services/sidebar_service.js @@ -4,9 +4,11 @@ import VueResource from 'vue-resource'; Vue.use(VueResource); export default class SidebarService { - constructor(endpoint) { + constructor(endpointMap) { if (!SidebarService.singleton) { - this.endpoint = endpoint; + this.endpoint = endpointMap.endpoint; + this.moveIssueEndpoint = endpointMap.moveIssueEndpoint; + this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint; SidebarService.singleton = this; } @@ -25,4 +27,18 @@ export default class SidebarService { emulateJSON: true, }); } + + getProjectsAutocomplete(searchTerm) { + return Vue.http.get(this.projectsAutocompleteEndpoint, { + params: { + search: searchTerm, + }, + }); + } + + moveIssue(moveToProjectId) { + return Vue.http.post(this.moveIssueEndpoint, { + move_to_project_id: moveToProjectId, + }); + } } diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js index 9edded3ead6..3d8972050a9 100644 --- a/app/assets/javascripts/sidebar/sidebar_bundle.js +++ b/app/assets/javascripts/sidebar/sidebar_bundle.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import sidebarTimeTracking from './components/time_tracking/sidebar_time_tracking'; import sidebarAssignees from './components/assignees/sidebar_assignees'; import confidential from './components/confidential/confidential_issue_sidebar.vue'; +import SidebarMoveIssue from './lib/sidebar_move_issue'; import Mediator from './sidebar_mediator'; @@ -31,6 +32,12 @@ function domContentLoaded() { service: mediator.service, }, }).$mount(confidentialEl); + + new SidebarMoveIssue( + mediator, + $('.js-move-issue'), + $('.js-move-issue-confirmation-button'), + ).init(); } new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker'); diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js index 721e92221cf..e38a8db4cc5 100644 --- a/app/assets/javascripts/sidebar/sidebar_mediator.js +++ b/app/assets/javascripts/sidebar/sidebar_mediator.js @@ -7,7 +7,11 @@ export default class SidebarMediator { constructor(options) { if (!SidebarMediator.singleton) { this.store = new Store(options); - this.service = new Service(options.endpoint); + this.service = new Service({ + endpoint: options.endpoint, + moveIssueEndpoint: options.moveIssueEndpoint, + projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint, + }); SidebarMediator.singleton = this; } @@ -26,6 +30,10 @@ export default class SidebarMediator { return this.service.update(field, selected.length === 0 ? [0] : selected); } + setMoveToProjectId(projectId) { + this.store.setMoveToProjectId(projectId); + } + fetch() { this.service.get() .then(response => response.json()) @@ -35,4 +43,23 @@ export default class SidebarMediator { }) .catch(() => new Flash('Error occured when fetching sidebar data')); } + + fetchAutocompleteProjects(searchTerm) { + return this.service.getProjectsAutocomplete(searchTerm) + .then(response => response.json()) + .then((data) => { + this.store.setAutocompleteProjects(data); + return this.store.autocompleteProjects; + }); + } + + moveIssue() { + return this.service.moveIssue(this.store.moveToProjectId) + .then(response => response.json()) + .then((data) => { + if (location.pathname !== data.web_url) { + gl.utils.visitUrl(data.web_url); + } + }); + } } diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js index 3356dd0191f..cc04a2a3fcf 100644 --- a/app/assets/javascripts/sidebar/stores/sidebar_store.js +++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js @@ -13,6 +13,8 @@ export default class SidebarStore { this.isFetching = { assignees: true, }; + this.autocompleteProjects = []; + this.moveToProjectId = 0; SidebarStore.singleton = this; } @@ -53,4 +55,12 @@ export default class SidebarStore { removeAllAssignees() { this.assignees = []; } + + setAutocompleteProjects(projects) { + this.autocompleteProjects = projects; + } + + setMoveToProjectId(moveToProjectId) { + this.moveToProjectId = moveToProjectId; + } } diff --git a/app/assets/javascripts/sidebar_height_manager.js b/app/assets/javascripts/sidebar_height_manager.js deleted file mode 100644 index 2752fe2b911..00000000000 --- a/app/assets/javascripts/sidebar_height_manager.js +++ /dev/null @@ -1,37 +0,0 @@ -import _ from 'underscore'; -import Cookies from 'js-cookie'; - -export default { - init() { - if (!this.initialized) { - if (Cookies.get('new_nav') === 'true' && $('.js-issuable-sidebar').length) return; - - this.$window = $(window); - this.$rightSidebar = $('.js-right-sidebar'); - this.$navHeight = $('.navbar-gitlab').outerHeight() + - $('.layout-nav').outerHeight() + - $('.sub-nav-scroll').outerHeight(); - - const throttledSetSidebarHeight = _.throttle(() => this.setSidebarHeight(), 20); - const debouncedSetSidebarHeight = _.debounce(() => this.setSidebarHeight(), 200); - - this.$window.on('scroll', throttledSetSidebarHeight); - this.$window.on('resize', debouncedSetSidebarHeight); - this.initialized = true; - } - }, - - setSidebarHeight() { - const currentScrollDepth = window.pageYOffset || 0; - const diff = this.$navHeight - currentScrollDepth; - - if (diff > 0) { - const newSidebarHeight = window.innerHeight - diff; - this.$rightSidebar.outerHeight(newSidebarHeight); - this.sidebarHeightIsCustom = true; - } else if (this.sidebarHeightIsCustom) { - this.$rightSidebar.outerHeight('100%'); - this.sidebarHeightIsCustom = false; - } - }, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js index c05a76a3b4a..aaca42e3ebc 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js @@ -75,18 +75,20 @@ export default { class="btn btn-small inline"> Check out branch </a> - <span class="dropdown inline prepend-left-10"> + <span class="dropdown prepend-left-10"> <a - class="btn btn-xs dropdown-toggle" + class="btn btn-small inline dropdown-toggle" data-toggle="dropdown" aria-label="Download as" role="button"> <i class="fa fa-download" - aria-hidden="true" /> + aria-hidden="true"> + </i> <i class="fa fa-caret-down" - aria-hidden="true" /> + aria-hidden="true"> + </i> </a> <ul class="dropdown-menu dropdown-menu-align-right"> <li> diff --git a/app/assets/javascripts/vue_shared/components/identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue index 0edd820743f..7cf2e029cf6 100644 --- a/app/assets/javascripts/vue_shared/components/identicon.vue +++ b/app/assets/javascripts/vue_shared/components/identicon.vue @@ -9,6 +9,11 @@ export default { type: String, required: true, }, + sizeClass: { + type: String, + required: false, + default: 's40', + }, }, computed: { /** @@ -38,7 +43,8 @@ export default { <template> <div - class="avatar s40 identicon" + class="avatar identicon" + :class="sizeClass" :style="identiconStyles"> {{identiconTitle}} </div> diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index b2b3297e880..c0524bf6aa3 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -51,3 +51,4 @@ @import "framework/snippets"; @import "framework/memory_graph"; @import "framework/responsive-tables"; +@import "framework/feature_highlight"; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index b4a6b214e98..82350c36df0 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -46,6 +46,15 @@ } } +@mixin btn-svg { + svg { + height: 15px; + width: 15px; + position: relative; + top: 2px; + } +} + @mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) { background-color: $light; border-color: $border-light; @@ -123,6 +132,7 @@ .btn { @include btn-default; @include btn-white; + @include btn-svg; color: $gl-text-color; @@ -222,13 +232,6 @@ } } - svg { - height: 15px; - width: 15px; - position: relative; - top: 2px; - } - svg, .fa { &:not(:last-child) { diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index 4ce767e4cc4..c165ec0b94b 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -95,8 +95,8 @@ .is-selected .pika-day, .pika-day:hover, .is-today .pika-day { - background: $gl-primary; - color: $white-light; + background: $gray-darker; + color: $gl-text-color; box-shadow: none; } } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 68a51c5a461..a85051642dd 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -21,6 +21,7 @@ .append-right-default { margin-right: $gl-padding; } .append-right-20 { margin-right: 20px; } .append-bottom-0 { margin-bottom: 0; } +.append-bottom-5 { margin-bottom: 5px; } .append-bottom-10 { margin-bottom: 10px; } .append-bottom-15 { margin-bottom: 15px; } .append-bottom-20 { margin-bottom: 20px; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 8872dec4c37..6b21def33a6 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -193,7 +193,7 @@ min-width: 240px; max-width: 500px; margin-top: 2px; - margin-bottom: 0; + margin-bottom: 2px; font-size: 14px; font-weight: $gl-font-weight-normal; padding: 8px 0; @@ -622,6 +622,11 @@ border-top: 1px solid $dropdown-divider-color; } +.dropdown-footer-content { + padding-left: 10px; + padding-right: 10px; +} + .dropdown-due-date-footer { padding-top: 0; margin-left: 10px; @@ -732,6 +737,8 @@ @mixin new-style-dropdown($selector: '') { #{$selector}.dropdown-menu, #{$selector}.dropdown-menu-nav { + margin-bottom: 24px; + li { display: block; padding: 0 1px; @@ -756,13 +763,15 @@ button, .menu-item { border-radius: 0; + box-shadow: none; padding: 8px 16px; text-align: left; + white-space: normal; width: 100%; // make sure the text color is not overriden &.text-danger { - @extend .text-danger; + color: $brand-danger; } &.is-focused, @@ -771,6 +780,11 @@ &:focus { background-color: $dropdown-item-hover-bg; color: $gl-text-color; + + // make sure the text color is not overriden + &.text-danger { + color: $brand-danger; + } } &.is-active { @@ -816,3 +830,152 @@ } @include new-style-dropdown('.js-namespace-select + '); + +header.navbar-gitlab-new .header-content .dropdown-menu.projects-dropdown-menu { + padding: 0; + + @media (max-width: $screen-xs-max) { + display: table; + left: -50px; + min-width: 300px; + } +} + +.projects-dropdown-container { + display: flex; + flex-direction: row; + width: 500px; + height: 334px; + + .project-dropdown-sidebar, + .project-dropdown-content { + padding: 8px 0; + } + + .loading-animation { + color: $almost-black; + } + + .project-dropdown-sidebar { + width: 30%; + border-right: 1px solid $border-color; + } + + .project-dropdown-content { + position: relative; + width: 70%; + } + + @media (max-width: $screen-xs-max) { + flex-direction: column; + width: 100%; + height: auto; + flex: 1; + + .project-dropdown-sidebar, + .project-dropdown-content { + width: 100%; + } + + .project-dropdown-sidebar { + border-bottom: 1px solid $border-color; + border-right: 0; + } + } +} + +.projects-dropdown-container { + .projects-list-frequent-container, + .projects-list-search-container, { + padding: 8px 0; + overflow-y: auto; + } + + .section-header, + .projects-list-frequent-container li.section-empty, + .projects-list-search-container li.section-empty { + padding: 0 15px; + } + + .section-header, + .projects-list-frequent-container li.section-empty, + .projects-list-search-container li.section-empty { + color: $gl-text-color-secondary; + font-size: $gl-font-size; + } + + .projects-list-frequent-container, + .projects-list-search-container { + li.section-empty.section-failure { + color: $callout-danger-color; + } + } + + .search-input-container { + position: relative; + padding: 4px $gl-padding; + + .search-icon { + position: absolute; + top: 13px; + right: 25px; + color: $md-area-border; + } + } + + .section-header { + font-weight: 700; + margin-top: 8px; + } + + .projects-list-search-container { + height: 284px; + } + + @media (max-width: $screen-xs-max) { + .projects-list-frequent-container { + width: auto; + height: auto; + padding-bottom: 0; + } + } +} + +.projects-list-item-container { + .project-item-avatar-container + .project-item-metadata-container { + float: left; + } + + .project-title, + .project-namespace { + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + } + + &:hover { + .project-item-avatar-container .avatar { + border-color: $md-area-border; + } + } + + .project-title { + font-size: $gl-font-size; + font-weight: 400; + line-height: 16px; + } + + .project-namespace { + margin-top: 4px; + font-size: 12px; + line-height: 12px; + color: $gl-text-color-secondary; + } + + @media (max-width: $screen-xs-max) { + .project-item-metadata-container { + float: none; + } + } +} diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss new file mode 100644 index 00000000000..ebae473df50 --- /dev/null +++ b/app/assets/stylesheets/framework/feature_highlight.scss @@ -0,0 +1,94 @@ +.feature-highlight { + position: relative; + margin-left: $gl-padding; + width: 20px; + height: 20px; + cursor: pointer; + + &::before { + content: ''; + display: block; + position: absolute; + top: 6px; + left: 6px; + width: 8px; + height: 8px; + background-color: $blue-500; + border-radius: 50%; + box-shadow: 0 0 0 rgba($blue-500, 0.4); + animation: pulse-highlight 2s infinite; + } + + &:hover::before, + &.disable-animation::before { + animation: none; + } + + &[disabled]::before { + display: none; + } +} + +.is-showing-fly-out { + .feature-highlight { + display: none; + } +} + +.feature-highlight-popover-content { + display: none; + + hr { + margin: $gl-padding * 0.5 0; + } + + .btn-link { + @include btn-svg; + + svg path { + fill: currentColor; + } + } + + .dismiss-feature-highlight { + padding: 0; + } + + svg:first-child { + width: 100%; + background-color: $indigo-50; + border-top-left-radius: 2px; + border-top-right-radius: 2px; + border-bottom: 1px solid darken($gray-normal, 8%); + } +} + +.popover .feature-highlight-popover-content { + display: block; +} + +.feature-highlight-popover { + padding: 0; + + .popover-content { + padding: 0; + } +} + +.feature-highlight-popover-sub-content { + padding: 9px 14px; +} + +@include keyframes(pulse-highlight) { + 0% { + box-shadow: 0 0 0 0 rgba($blue-200, 0.4); + } + + 70% { + box-shadow: 0 0 0 10px transparent; + } + + 100% { + box-shadow: 0 0 0 0 transparent; + } +} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 35bd97980e2..b00a2d053e2 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -105,12 +105,11 @@ header { top: -3px; font-size: 10px; } + } + .user-counter { svg { - position: relative; - top: 2px; - height: 17px; - // hack to get SVG to line up with FA icons + height: 16px; width: 23px; fill: currentColor; } @@ -325,12 +324,12 @@ header { li { .badge { position: inherit; - top: -8px; font-weight: $gl-font-weight-normal; - margin-left: -11px; + margin-left: -6px; font-size: 11px; color: $white-light; - padding: 1px 5px 2px; + padding: 0 5px; + line-height: 12px; border-radius: 7px; box-shadow: 0 1px 0 rgba($gl-header-color, .2); diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index a39927eb0df..6c14e8b97e0 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -267,14 +267,26 @@ // TODO: change global style .ajax-project-dropdown, +.ajax-users-dropdown, +body[data-page="projects:edit"] #select2-drop, body[data-page="projects:new"] #select2-drop, +body[data-page="projects:merge_requests:edit"] #select2-drop, body[data-page="projects:blob:new"] #select2-drop, body[data-page="profiles:show"] #select2-drop, +body[data-page="admin:groups:show"] #select2-drop, +body[data-page="projects:issues:show"] #select2-drop, body[data-page="projects:blob:edit"] #select2-drop { &.select2-drop { + border: 1px solid $dropdown-border-color; + border-radius: $border-radius-base; color: $gl-text-color; } + &.select2-drop-above { + border-top: none; + margin-top: -4px; + } + .select2-results { .select2-no-results, .select2-searching, diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 40e8a928e6e..ef58382ba41 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -132,3 +132,7 @@ width: calc(100% + 35px); } } + +.issuable-sidebar { + @include new-style-dropdown; +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 01fffa717e9..88b08998dfd 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -177,13 +177,14 @@ $row-hover: $blue-25; $row-hover-border: $blue-100; $progress-color: #c0392b; $header-height: 50px; +$new-navbar-height: 40px; $fixed-layout-width: 1280px; $limited-layout-width: 990px; $limited-layout-width-sm: 790px; $container-text-max-width: 540px; $gl-avatar-size: 40px; $error-exclamation-point: $red-500; -$border-radius-default: 3px; +$border-radius-default: 4px; $settings-icon-size: 18px; $provider-btn-not-active-color: $blue-500; $link-underline-blue: $blue-500; diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss index b711bd12c73..4deb7431284 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -2,15 +2,21 @@ @import 'framework/tw_bootstrap_variables'; @import "bootstrap/variables"; +.content-wrapper.page-with-new-nav { + margin-top: $new-navbar-height; +} + header.navbar-gitlab-new { color: $white-light; background: linear-gradient(to right, $indigo-900, $indigo-800); border-bottom: 0; + min-height: $new-navbar-height; .header-content { display: -webkit-flex; display: flex; padding-left: 0; + min-height: $new-navbar-height; .title-container { display: -webkit-flex; @@ -38,20 +44,13 @@ header.navbar-gitlab-new { display: -webkit-flex; display: flex; align-items: center; - padding-right: $gl-padding; - padding-left: $gl-padding; - margin-left: -$gl-padding; - - @media (min-width: $screen-sm-min) { - padding-right: $gl-padding; - padding-left: $gl-padding; - } + padding: 2px 8px; + margin: 5px 2px 5px -8px; + border-radius: $border-radius-default; svg { - margin-top: -3px; - @media (min-width: $screen-sm-min) { - margin-right: 10px; + margin-right: 8px; } } @@ -60,7 +59,7 @@ header.navbar-gitlab-new { svg { width: 55px; - height: 15px; + height: 14px; margin: 0; fill: $white-light; } @@ -68,9 +67,7 @@ header.navbar-gitlab-new { &:hover, &:focus { - .logo-text svg { - fill: $tanuki-yellow; - } + background-color: rgba($indigo-200, .2); } } } @@ -90,6 +87,20 @@ header.navbar-gitlab-new { right: 0; } } + + &.menu-expanded { + @media (max-width: $screen-xs-max) { + .title-container, + .header-logo, { + display: none; + } + } + } + } + + .dropdown-bold-header { + color: $gl-text-color-secondary; + font-size: 12px; } .navbar-collapse { @@ -98,14 +109,10 @@ header.navbar-gitlab-new { box-shadow: 0; @media (max-width: $screen-xs-max) { - margin-left: -$gl-padding; + margin-left: -8px; margin-right: -10px; } - .dropdown-bold-header { - color: initial; - } - .nav { > li:not(.hidden-xs) a { @media (max-width: $screen-xs-max) { @@ -119,7 +126,7 @@ header.navbar-gitlab-new { .container-fluid { .navbar-toggle { min-width: 45px; - padding: 6px $gl-padding; + padding: 4px $gl-padding; margin-right: -7px; font-size: 14px; text-align: center; @@ -156,31 +163,90 @@ header.navbar-gitlab-new { } > a { - background: none; will-change: color; + margin: 4px 2px; + padding: 6px 8px; + color: $indigo-200; + height: 32px; + + @media (max-width: $screen-xs-max) { + padding: 0; + } + + svg { + fill: $indigo-200; + } &.header-user-dropdown-toggle { + margin-left: 2px; + .header-user-avatar { border-color: $indigo-200; + margin-right: 0; } } + } - &:hover, - &:focus { - color: $white-light; - opacity: 1; + .header-new-dropdown-toggle { + margin-right: 0; + } - > svg { - fill: $white-light; - } + > a:hover, + > a:focus { + text-decoration: none; + outline: 0; + opacity: 1; + color: $white-light; + + @media (min-width: $screen-sm-min) { + background-color: rgba($indigo-200, .2); + } + + svg { + fill: currentColor; + } - &.header-user-dropdown-toggle { - .header-user-avatar { - border-color: $white-light; - } + &.header-user-dropdown-toggle { + .header-user-avatar { + border-color: $white-light; } } } + + .impersonated-user, + .impersonated-user:hover { + margin-right: 1px; + background-color: $white-light; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + + svg { + fill: $indigo-900; + } + } + + .impersonation-btn, + .impersonation-btn:hover { + background-color: $white-light; + margin-left: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + + i { + color: $orange-500; + font-size: 20px; + } + } + + &.active > a, + &.dropdown.open > a { + color: $indigo-900; + background-color: $white-light; + + svg { + fill: currentColor; + } + } } } } @@ -188,45 +254,76 @@ header.navbar-gitlab-new { .navbar-sub-nav { display: -webkit-flex; display: flex; - margin-bottom: 0; + margin: 0 0 0 6px; color: $indigo-200; - > li { - > a:hover, - > a:focus { - box-shadow: inset 0 -3px 0 rgba($indigo-200, .4); - text-decoration: none; - outline: 0; - color: $white-light; - } + .dropdown-chevron { + position: relative; + top: -1px; + font-size: 10px; + } +} - &.active > a { - box-shadow: inset 0 -3px 0 $indigo-500; - color: $white-light; - font-weight: $gl-font-weight-bold; - } +.navbar-gitlab-new { + .navbar-sub-nav, + .navbar-nav { + > li { + > a:hover, + > a:focus { + text-decoration: none; + outline: 0; + color: $white-light; + background-color: rgba($indigo-200, .2); - > a { - display: block; - padding: 16px 10px; - font-size: 13px; - color: currentColor; - box-shadow: inset 0 0 0 transparent; - will-change: box-shadow; - transition: box-shadow 0.15s; + svg { + fill: currentColor; + } + } - @media (min-width: $screen-sm-min) { - padding: 15px $gl-padding; - font-size: 14px; + &.active > a, + &.dropdown.open > a { + color: $indigo-900; + background-color: $white-light; + + svg { + fill: currentColor; + } + } + + > a { + display: flex; + align-items: center; + justify-content: center; + padding: 6px 8px; + margin: 4px 2px; + font-size: 12px; + color: currentColor; + border-radius: $border-radius-default; + height: 32px; + font-weight: $gl-font-weight-bold; + + svg { + fill: currentColor; + } + } + + &.line-separator { + border-left: 1px solid rgba($indigo-200, .2); + margin: 8px; } } } +} - .dropdown-chevron { - position: relative; - top: -1px; - font-size: 10px; - } +.admin-icon i { + font-size: 18px; +} + +.caret-down { + height: 11px; + width: 11px; + margin-left: 4px; + fill: currentColor; } .header-user .dropdown-menu-nav, @@ -235,10 +332,14 @@ header.navbar-gitlab-new { } .search { + margin: 4px 8px 0; + form { + height: 32px; border: 0; + border-radius: $border-radius-default; 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; + transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s; &:hover { background-color: rgba($indigo-200, .3); @@ -247,31 +348,50 @@ header.navbar-gitlab-new { } &.search-active form { - background-color: rgba($indigo-200, .3); + background-color: $white-light; box-shadow: none; + + .search-input { + color: $gl-text-color; + transition: color ease-in-out 0.15s; + } + + .search-input::placeholder { + color: $gl-text-color-tertiary; + } + + .search-input-wrap { + .search-icon, + .clear-icon { + color: $gl-text-color-tertiary; + transition: color ease-in-out 0.15s; + } + } } .search-input { color: $white-light; background: none; + transition: color ease-in-out 0.15s; } .search-input::placeholder { color: rgba($indigo-200, .8); + transition: color ease-in-out 0.15s; } .location-badge { font-size: 12px; 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; + height: 32px; + transition: border-color ease-in-out 0.15s; } .search-input-wrap { @@ -283,8 +403,9 @@ header.navbar-gitlab-new { &.search-active { .location-badge { - color: $white-light; - background-color: rgba($indigo-200, .2); + color: $gl-text-color; + background-color: $nav-badge-bg; + border-color: $border-color; } .search-input-wrap { @@ -458,3 +579,14 @@ header.navbar-gitlab-new { } } } + +.btn-sign-in { + margin-top: 3px; + background-color: $indigo-100; + color: $indigo-900; + font-weight: $gl-font-weight-bold; + + &:hover { + background-color: $white-light; + } +} diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index f624b130e19..90b0a543c5c 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -26,7 +26,7 @@ $new-sidebar-collapsed-width: 50px; // Override position: absolute .right-sidebar { position: fixed; - height: calc(100% - #{$header-height}); + height: calc(100% - #{$new-navbar-height}); } .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header { @@ -93,7 +93,7 @@ $new-sidebar-collapsed-width: 50px; z-index: 400; width: $new-sidebar-width; transition: left $sidebar-transition-duration; - top: $header-height; + top: $new-navbar-height; bottom: 0; left: 0; background-color: $gray-normal; @@ -189,7 +189,7 @@ $new-sidebar-collapsed-width: 50px; } .with-performance-bar .nav-sidebar { - top: $header-height + $performance-bar-height; + top: $new-navbar-height + $performance-bar-height; } .sidebar-sub-level-items { @@ -453,7 +453,7 @@ $new-sidebar-collapsed-width: 50px; // Make issue boards full-height now that sub-nav is gone .boards-list { - height: calc(100vh - #{$header-height}); + height: calc(100vh - #{$new-navbar-height}); @media (min-width: $screen-sm-min) { height: 475px; // Needed for PhantomJS @@ -464,7 +464,7 @@ $new-sidebar-collapsed-width: 50px; } .with-performance-bar .boards-list { - height: calc(100vh - #{$header-height} - #{$performance-bar-height}); + height: calc(100vh - #{$new-navbar-height} - #{$performance-bar-height}); } diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 0f3074076ce..314dd2d1a21 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -440,6 +440,7 @@ &.right-sidebar { top: 0; bottom: 0; + height: 100%; } .issuable-sidebar-header { diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 3d04df8d820..50ec5110bf1 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -322,14 +322,13 @@ } .build-dropdown { - padding: $gl-padding 0; + @include new-style-dropdown; - .dropdown-menu-toggle { - margin-top: 8px; - } + margin: $gl-padding 0; + padding: 0; - .dropdown-menu { - margin-top: -$gl-padding; + .dropdown-menu-toggle { + margin-top: #{$gl-padding / 2}; } svg { diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index a8d2ae0af28..9362d80d4e6 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -12,6 +12,8 @@ .environments-container { .ci-table { + @include new-style-dropdown; + .deployment-column { > span { word-break: break-all; @@ -167,7 +169,7 @@ } .metric-area { - opacity: 0.8; + opacity: 0.25; } .prometheus-graph-overlay { @@ -225,6 +227,26 @@ margin-top: 20px; } +.prometheus-graph-group { + display: flex; + flex-wrap: wrap; + padding: $gl-padding / 2; +} + +.prometheus-graph { + flex: 1 0 auto; + min-width: 450px; + padding: $gl-padding / 2; + + h5 { + font-size: 16px; + } + + @media (max-width: $screen-sm-max) { + min-width: 100%; + } +} + .prometheus-svg-container { position: relative; height: 0; @@ -249,8 +271,14 @@ font-weight: $gl-font-weight-bold; } - .label-axis-text, - .text-metric-usage { + .label-axis-text { + fill: $black; + font-weight: $gl-font-weight-normal; + font-size: 10px; + } + + .text-metric-usage, + .legend-metric-title { fill: $black; font-weight: $gl-font-weight-normal; font-size: 12px; @@ -289,9 +317,3 @@ } } } - -.prometheus-row { - h5 { - font-size: 16px; - } -} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 4b0b238a767..9f2cb979518 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -473,7 +473,7 @@ padding-top: 6px; } - .open .dropdown-menu { + .dropdown-menu { width: 100%; } } @@ -486,6 +486,24 @@ } } +.sidebar-move-issue-dropdown { + @include new-style-dropdown; +} + +.sidebar-move-issue-confirmation-button { + width: 100%; + + &.is-loading { + .sidebar-move-issue-confirmation-loading-icon { + display: inline-block; + } + } +} + +.sidebar-move-issue-confirmation-loading-icon { + display: none; +} + .detail-page-description { padding: 16px 0; @@ -599,6 +617,8 @@ } .issuable-actions { + @include new-style-dropdown; + padding-top: 10px; @media (min-width: $screen-sm-min) { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 518bb270b88..e8ca5cedaee 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -143,8 +143,12 @@ ul.related-merge-requests > li { } } -.issue-form .select2-container { - width: 250px !important; +.issue-form { + @include new-style-dropdown; + + .select2-container { + width: 250px !important; + } } .issues-footer { @@ -186,6 +190,8 @@ ul.related-merge-requests > li { } .create-mr-dropdown-wrap { + @include new-style-dropdown; + .btn-group:not(.hide) { display: flex; } @@ -212,15 +218,6 @@ ul.related-merge-requests > li { } li:not(.divider) { - padding: 6px; - cursor: pointer; - - &:hover, - &:focus { - background-color: $dropdown-hover-color; - color: $white-light; - } - &.droplab-item-selected { .icon-container { i { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index d7f53a74825..8609f72bdab 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -721,3 +721,7 @@ font-size: 16px; } } + +.merge-request-form { + @include new-style-dropdown; +} diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 8932cff22a8..5d7c85b16ef 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -23,6 +23,8 @@ .new-note, .note-edit-form { .note-form-actions { + @include new-style-dropdown; + position: relative; margin: $gl-padding 0 0; } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 9f6b363568e..cb8815e4775 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -257,6 +257,8 @@ // Pipeline visualization .pipeline-actions { + @include new-style-dropdown; + border-bottom: none; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 19caefa1961..dd600a27545 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -800,8 +800,10 @@ pre.light-well { } } -.new_protected_branch, +.new-protected-branch, .new-protected-tag { + @include new-style-dropdown; + label { margin-top: 6px; font-weight: $gl-font-weight-normal; @@ -821,19 +823,9 @@ pre.light-well { .protected-branches-list, .protected-tags-list { - margin-bottom: 30px; - - a { - color: $gl-text-color; - - &:hover { - color: $gl-link-color; - } + @include new-style-dropdown; - &.is-active { - font-weight: $gl-font-weight-bold; - } - } + margin-bottom: 30px; .settings-message { margin: 0; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 8d73246223d..615020ca856 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -190,6 +190,8 @@ input[type="checkbox"]:hover { } .search-holder { + @include new-style-dropdown; + @media (min-width: $screen-sm-min) { display: -webkit-flex; display: flex; diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 59be955599d..dfc8bd0ba81 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -41,12 +41,6 @@ class AutocompleteController < ApplicationController project = Project.find_by_id(params[:project_id]) projects = projects_finder.execute(project, search: params[:search], offset_id: params[:offset_id]) - no_project = { - id: 0, - name_with_namespace: 'No project' - } - projects.unshift(no_project) unless params[:offset_id].present? - render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace) end diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index a34a82b7ba6..23909bd2d39 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -36,6 +36,34 @@ module IssuableCollections @merge_requests_finder ||= issuable_finder_for(MergeRequestsFinder) end + def redirect_out_of_range(relation, total_pages) + return false if total_pages.zero? + + out_of_range = relation.current_page > total_pages + + if out_of_range + redirect_to(url_for(params.merge(page: total_pages, only_path: true))) + end + + out_of_range + end + + def issues_page_count(relation) + page_count_for_relation(relation, issues_finder.row_count) + end + + def merge_requests_page_count(relation) + page_count_for_relation(relation, merge_requests_finder.row_count) + end + + def page_count_for_relation(relation, row_count) + limit = relation.limit_value.to_f + + return 1 if limit.zero? + + (row_count.to_f / limit).ceil + end + def issuable_finder_for(finder_class) finder_class.new(current_user, filter_params) end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 349b19f72e2..dc9e6f71152 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -15,7 +15,7 @@ class Projects::IssuesController < Projects::ApplicationController before_action :authorize_create_issue!, only: [:new, :create] # Allow modify issue - before_action :authorize_update_issue!, only: [:edit, :update] + before_action :authorize_update_issue!, only: [:edit, :update, :move] # Allow create a new branch and empty WIP merge request from current issue before_action :authorize_create_merge_request!, only: [:create_merge_request] @@ -27,10 +27,9 @@ class Projects::IssuesController < Projects::ApplicationController @issues = issues_collection @issues = @issues.page(params[:page]) @issuable_meta_data = issuable_meta_data(@issues, @collection_type) + @total_pages = issues_page_count(@issues) - if @issues.out_of_range? && @issues.total_pages != 0 - return redirect_to url_for(params.merge(page: @issues.total_pages, only_path: true)) - end + return if redirect_out_of_range(@issues, @total_pages) if params[:label_name].present? @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute @@ -142,25 +141,33 @@ class Projects::IssuesController < Projects::ApplicationController @issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue) + respond_to do |format| + format.html do + recaptcha_check_with_fallback { render :edit } + end + + format.json do + render_issue_json + end + end + + rescue ActiveRecord::StaleObjectError + render_conflict_response + end + + def move + params.require(:move_to_project_id) + if params[:move_to_project_id].to_i > 0 new_project = Project.find(params[:move_to_project_id]) return render_404 unless issue.can_move?(current_user, new_project) - move_service = Issues::MoveService.new(project, current_user) - @issue = move_service.execute(@issue, new_project) + @issue = Issues::UpdateService.new(project, current_user, target_project: new_project).execute(issue) end respond_to do |format| - format.html do - recaptcha_check_with_fallback { render :edit } - end - format.json do - if @issue.valid? - render json: serializer.represent(@issue) - else - render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity - end + render_issue_json end end @@ -271,6 +278,14 @@ class Projects::IssuesController < Projects::ApplicationController return render_404 unless @project.feature_available?(:issues, current_user) end + def render_issue_json + if @issue.valid? + render json: serializer.represent(@issue) + else + render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity + end + end + def issue_params params.require(:issue).permit(*issue_params_attributes) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index e3fa3736808..5095d7fd445 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -18,10 +18,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo @merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.preload(merge_request_diff: :merge_request) @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) + @total_pages = merge_requests_page_count(@merge_requests) - if @merge_requests.out_of_range? && @merge_requests.total_pages != 0 - return redirect_to url_for(params.merge(page: @merge_requests.total_pages, only_path: true)) - end + return if redirect_out_of_range(@merge_requests, @total_pages) if params[:label_name].present? labels_params = { project_id: @project.id, title: params[:label_name] } diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ed17b3b4689..b13034d3333 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -323,6 +323,7 @@ class ProjectsController < Projects::ApplicationController :build_allow_git_fetch, :build_coverage_regex, :build_timeout_in_minutes, + :resolve_outdated_diff_discussions, :container_registry_enabled, :default_branch, :description, diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index c8dd2275730..9848497f258 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -61,6 +61,10 @@ class IssuableFinder execute.find_by(*params) end + def row_count + Gitlab::IssuablesCountForState.new(self).for_state_or_opened(params[:state]) + end + # We often get counts for each state by running a query per state, and # counting those results. This is typically slower than running one query # (even if that query is slower than any of the individual state queries) and diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index aa9cef6b08c..d2275139c42 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -14,6 +14,7 @@ # search: string # label_name: string # sort: string +# my_reaction_emoji: string # class IssuesFinder < IssuableFinder CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 771da3d441d..d0687d28c21 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -16,6 +16,7 @@ # label_name: string # sort: string # non_archived: boolean +# my_reaction_emoji: string # class MergeRequestsFinder < IssuableFinder def klass diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 017df8f6794..8d02d5de5c3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -302,10 +302,6 @@ module ApplicationHelper end end - def show_new_nav? - true - end - def collapsed_sidebar? cookies["sidebar_collapsed"] == "true" end diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index ff305fa39b4..5089da519df 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -97,9 +97,11 @@ module DropdownsHelper end end - def dropdown_footer(&block) + def dropdown_footer(add_content_class: false, &block) content_tag(:div, class: "dropdown-footer") do - if block + if add_content_class + content_tag(:div, capture(&block), class: "dropdown-footer-content") + else capture(&block) end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index dd159d12aa0..c4c41eac5db 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -68,7 +68,7 @@ module GroupsHelper def group_title_link(group, hidable: false) link_to(group_path(group), class: "group-path #{'hidable' if hidable}") do output = - if show_new_nav? && !Rails.env.test? + if !Rails.env.test? image_tag(group_icon(group), class: "avatar-tile", width: 16, height: 16) else "" diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 0fcd3347095..717abf2082d 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -207,12 +207,10 @@ module IssuablesHelper endpoint: project_issue_path(@project, issuable), canUpdate: can?(current_user, :update_issue, issuable), canDestroy: can?(current_user, :destroy_issue, issuable), - canMove: current_user ? issuable.can_move?(current_user) : false, issuableRef: issuable.to_reference, isConfidential: issuable.confidential, markdownPreviewPath: preview_markdown_path(@project), markdownDocsPath: help_page_path('user/markdown'), - projectsAutocompletePath: autocomplete_projects_path(project_id: @project.id), issuableTemplates: issuable_templates(issuable), projectPath: ref_project.path, projectNamespace: ref_project.namespace.full_path, @@ -242,7 +240,8 @@ module IssuablesHelper def issuables_count_for_state(issuable_type, state) finder = public_send("#{issuable_type}_finder") # rubocop:disable GitlabSecurity/PublicSend - finder.count_by_state[state] + + Gitlab::IssuablesCountForState.new(finder)[state] end def close_issuable_url(issuable) @@ -298,14 +297,6 @@ module IssuablesHelper cookies[:collapsed_gutter] == 'true' end - def issuable_state_scope(issuable) - if issuable.respond_to?(:merged?) && issuable.merged? - :merged - else - issuable.open? ? :opened : :closed - end - end - def issuable_templates(issuable) @issuable_templates ||= case issuable @@ -354,6 +345,8 @@ module IssuablesHelper def issuable_sidebar_options(issuable, can_edit_issuable) { endpoint: "#{issuable_json_path(issuable)}?basic=true", + moveIssueEndpoint: move_namespace_project_issue_path(namespace_id: issuable.project.namespace.to_param, project_id: issuable.project, id: issuable), + projectsAutocompleteEndpoint: autocomplete_projects_path(project_id: @project.id), editable: can_edit_issuable, currentUser: current_user.as_json(only: [:username, :id, :name], methods: :avatar_url), rootPath: root_path, diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 853ce827061..3d0fdce6a43 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -47,13 +47,6 @@ module IssuesHelper end end - def bulk_update_milestone_options - milestones = @project.milestones.active.reorder(due_date: :asc, title: :asc).to_a - milestones.unshift(Milestone::None) - - options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id]) - end - def milestone_options(object) milestones = object.project.milestones.active.reorder(due_date: :asc, title: :asc).to_a milestones.unshift(object.milestone) if object.milestone.present? && object.milestone.closed? @@ -93,14 +86,6 @@ module IssuesHelper return 'hidden' if issue.closed? == closed end - def merge_requests_sentence(merge_requests) - # Sorting based on the `!123` or `group/project!123` reference will sort - # local merge requests first. - merge_requests.map do |merge_request| - merge_request.to_reference(@project) - end.sort.to_sentence(last_word_connector: ', or ') - end - def confidential_icon(issue) icon('eye-slash') if issue.confidential? end @@ -148,18 +133,6 @@ module IssuesHelper end.to_h end - def due_date_options - options = [ - Issue::AnyDueDate, - Issue::NoDueDate, - Issue::DueThisWeek, - Issue::DueThisMonth, - Issue::Overdue - ] - - options_from_collection_for_select(options, 'name', 'title', params[:due_date]) - end - def link_to_discussions_to_resolve(merge_request, single_discussion = nil) link_text = merge_request.to_reference link_text += " (discussion #{single_discussion.first_note.id})" if single_discussion diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index b63b3b70903..a23a43c9f43 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -1,8 +1,8 @@ module NavHelper def page_with_sidebar_class class_name = page_gutter_class - class_name << 'page-with-new-sidebar' if defined?(@new_sidebar) && @new_sidebar - class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @new_sidebar + class_name << 'page-with-new-sidebar' if defined?(@left_sidebar) && @left_sidebar + class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar class_name end @@ -30,24 +30,15 @@ module NavHelper end end - def nav_header_class - class_names = [] - class_names << 'with-horizontal-nav' if defined?(nav) && nav - - class_names + def nav_control_class + "nav-control" if current_user end - def layout_nav_class - return [] if show_new_nav? - + def user_dropdown_class class_names = [] - class_names << 'page-with-layout-nav' if defined?(nav) && nav - class_names << 'page-with-sub-nav' if content_for?(:sub_nav) + class_names << 'header-user-dropdown-toggle' + class_names << 'impersonated-user' if session[:impersonator_id] class_names end - - def nav_control_class - "nav-control" if current_user - end end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 8c5e258f519..083ace65b09 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -146,4 +146,8 @@ module NotesHelper autocomplete: autocomplete } end + + def discussion_resolved_intro(discussion) + discussion.resolved_by_push? ? 'Automatically resolved' : 'Resolved' + end end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index b30b2eb1d03..d83c7411c9d 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -4,7 +4,7 @@ module PageLayoutHelper @page_title.push(*titles.compact) if titles.any? - if show_new_nav? && titles.any? && !defined?(@breadcrumb_title) + if titles.any? && !defined?(@breadcrumb_title) @breadcrumb_title = @page_title.last end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 0bf94fd30db..cec7656773c 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -62,7 +62,7 @@ module ProjectsHelper project_link = link_to project_path(project), { class: "project-item-select-holder" } do output = - if show_new_nav? && !Rails.env.test? + if !Rails.env.test? project_icon(project, alt: project.name, class: 'avatar-tile', width: 16, height: 16) else "" @@ -72,12 +72,6 @@ module ProjectsHelper output.html_safe end - if current_user - project_link << button_tag(type: 'button', class: 'dropdown-toggle-caret js-projects-dropdown-toggle', aria: { label: 'Toggle switch project dropdown' }, data: { target: '.js-dropdown-menu-projects', toggle: 'dropdown', order_by: 'last_activity_at' }) do - icon("chevron-down") - end - end - "#{namespace_link} / #{project_link}".html_safe end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index ae0e0aa3cf9..98e824a8c65 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -10,6 +10,7 @@ module SearchHelper search_pattern = Regexp.new(Regexp.escape(term), "i") generic_results = project_autocomplete + default_autocomplete + help_autocomplete + generic_results.concat(default_autocomplete_admin) if current_user.admin? generic_results.select! { |result| result[:label] =~ search_pattern } [ @@ -41,8 +42,14 @@ module SearchHelper [ { category: "Settings", label: "User settings", url: profile_path }, { category: "Settings", label: "SSH Keys", url: profile_keys_path }, - { category: "Settings", label: "Dashboard", url: root_path }, - { category: "Settings", label: "Admin Section", url: admin_root_path } + { category: "Settings", label: "Dashboard", url: root_path } + ] + end + + # Autocomplete results for settings pages, for admins + def default_autocomplete_admin + [ + { category: "Settings", label: "Admin Section", url: admin_root_path } ] end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index ba3156154ac..28c16d4037f 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -451,6 +451,10 @@ module Ci trace end + def serializable_hash(options = {}) + super(options).merge(when: read_attribute(:when)) + end + private def update_artifacts_size diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index ca9a350ea79..35d14b6e297 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -305,6 +305,10 @@ module Ci @stage_seeds ||= config_processor.stage_seeds(self) end + def has_kubernetes_active? + project.kubernetes_service&.active? + end + def has_stage_seeds? stage_seeds.any? end diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb index c58ce5c3717..2c860598281 100644 --- a/app/models/ci/trigger_request.rb +++ b/app/models/ci/trigger_request.rb @@ -6,6 +6,10 @@ module Ci belongs_to :pipeline, foreign_key: :commit_id has_many :builds + # We switched to Ci::PipelineVariable from Ci::TriggerRequest.variables. + # Ci::TriggerRequest doesn't save variables anymore. + validates :variables, absence: true + serialize :variables # rubocop:disable Cop/ActiveRecordSerialize def user_variables diff --git a/app/models/commit.rb b/app/models/commit.rb index c943365016f..ba3845df867 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -405,6 +405,6 @@ class Commit end def gpg_commit - @gpg_commit ||= Gitlab::Gpg::Commit.for_commit(self) + @gpg_commit ||= Gitlab::Gpg::Commit.new(self) end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 842c6e5cb50..f3888528940 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -38,6 +38,14 @@ class CommitStatus < ActiveRecord::Base scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } scope :after_stage, -> (index) { where('stage_idx > ?', index) } + enum failure_reason: { + unknown_failure: nil, + script_failure: 1, + api_failure: 2, + stuck_or_timeout_failure: 3, + runner_system_failure: 4 + } + state_machine :status do event :process do transition [:skipped, :manual] => :created @@ -79,6 +87,11 @@ class CommitStatus < ActiveRecord::Base commit_status.finished_at = Time.now end + before_transition any => :failed do |commit_status, transition| + failure_reason = transition.args.first + commit_status.failure_reason = failure_reason + end + after_transition do |commit_status, transition| next if transition.loopback? diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 3731b7c8577..681c3241dbb 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -6,6 +6,7 @@ # module Issuable extend ActiveSupport::Concern + include Gitlab::SQL::Pattern include CacheMarkdownField include Participable include Mentionable @@ -122,7 +123,9 @@ module Issuable # # Returns an ActiveRecord::Relation. def search(query) - where(arel_table[:title].matches("%#{query}%")) + title = to_fuzzy_arel(:title, query) + + where(title) end # Searches for records with a matching title or description. @@ -133,10 +136,10 @@ module Issuable # # Returns an ActiveRecord::Relation. def full_search(query) - t = arel_table - pattern = "%#{query}%" + title = to_fuzzy_arel(:title, query) + description = to_fuzzy_arel(:description, query) - where(t[:title].matches(pattern).or(t[:description].matches(pattern))) + where(title&.or(description)) end def sort(method, excluded_labels: []) diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb index dd979e7bb17..f006a271327 100644 --- a/app/models/concerns/resolvable_discussion.rb +++ b/app/models/concerns/resolvable_discussion.rb @@ -24,6 +24,7 @@ module ResolvableDiscussion delegate :resolved_at, :resolved_by, + :resolved_by_push?, to: :last_resolved_note, allow_nil: true diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb index 05eb6f86704..668c5a079e3 100644 --- a/app/models/concerns/resolvable_note.rb +++ b/app/models/concerns/resolvable_note.rb @@ -51,22 +51,34 @@ module ResolvableNote end # If you update this method remember to also update `.resolve!` - def resolve!(current_user) - return unless resolvable? - return if resolved? + def resolve_without_save(current_user, resolved_by_push: false) + return false unless resolvable? + return false if resolved? self.resolved_at = Time.now self.resolved_by = current_user - save! + self.resolved_by_push = resolved_by_push + + true end # If you update this method remember to also update `.unresolve!` - def unresolve! - return unless resolvable? - return unless resolved? + def unresolve_without_save + return false unless resolvable? + return false unless resolved? self.resolved_at = nil self.resolved_by = nil - save! + + true + end + + def resolve!(current_user, resolved_by_push: false) + resolve_without_save(current_user, resolved_by_push: resolved_by_push) && + save! + end + + def unresolve! + unresolve_without_save && save! end end diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 3df60ddc950..1633acd4fa9 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -56,7 +56,7 @@ class GpgKey < ActiveRecord::Base def verified_user_infos user_infos.select do |user_info| - user_info[:email] == user.email + user.verified_email?(user_info[:email]) end end @@ -64,13 +64,17 @@ class GpgKey < ActiveRecord::Base user_infos.map do |user_info| [ user_info[:email], - user_info[:email] == user.email + user.verified_email?(user_info[:email]) ] end.to_h end def verified? - emails_with_verified_status.any? { |_email, verified| verified } + emails_with_verified_status.values.any? + end + + def verified_and_belongs_to_email?(email) + emails_with_verified_status.fetch(email, false) end def update_invalid_gpg_signatures @@ -78,11 +82,14 @@ class GpgKey < ActiveRecord::Base end def revoke - GpgSignature.where(gpg_key: self, valid_signature: true).update_all( - gpg_key_id: nil, - valid_signature: false, - updated_at: Time.zone.now - ) + GpgSignature + .where(gpg_key: self) + .where.not(verification_status: GpgSignature.verification_statuses[:unknown_key]) + .update_all( + gpg_key_id: nil, + verification_status: GpgSignature.verification_statuses[:unknown_key], + updated_at: Time.zone.now + ) destroy end diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index 50fb35c77ec..454c90d5fc4 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -1,9 +1,21 @@ class GpgSignature < ActiveRecord::Base include ShaAttribute + include IgnorableColumn + + ignore_column :valid_signature sha_attribute :commit_sha sha_attribute :gpg_key_primary_keyid + enum verification_status: { + unverified: 0, + verified: 1, + same_user_different_email: 2, + other_user: 3, + unverified_key: 4, + unknown_key: 5 + } + belongs_to :project belongs_to :gpg_key @@ -20,6 +32,6 @@ class GpgSignature < ActiveRecord::Base end def gpg_commit - Gitlab::Gpg::Commit.new(project, commit_sha) + Gitlab::Gpg::Commit.new(commit) end end diff --git a/app/models/group.rb b/app/models/group.rb index 190b27cf66b..e746e4a12c9 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -16,6 +16,7 @@ class Group < Namespace source: :user has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent + has_many :members_and_requesters, as: :source, class_name: 'GroupMember' has_many :milestones has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent diff --git a/app/models/member.rb b/app/models/member.rb index ee2cb13697b..cbbd58f2eaf 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -126,20 +126,11 @@ class Member < ActiveRecord::Base find_by(invite_token: invite_token) end - def add_user(source, user, access_level, current_user: nil, expires_at: nil) - user = retrieve_user(user) + def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil) + # `user` can be either a User object, User ID or an email to be invited + member = retrieve_member(source, user, existing_members) access_level = retrieve_access_level(access_level) - # `user` can be either a User object or an email to be invited - member = - if user.is_a?(User) - source.members.find_by(user_id: user.id) || - source.requesters.find_by(user_id: user.id) || - source.members.build(user_id: user.id) - else - source.members.build(invite_email: user) - end - return member unless can_update_member?(current_user, member) member.attributes = { @@ -165,17 +156,15 @@ class Member < ActiveRecord::Base def add_users(source, users, access_level, current_user: nil, expires_at: nil) return [] unless users.present? - # Collect all user ids into separate array - # so we can use single sql query to get user objects - user_ids = users.select { |user| user =~ /\A\d+\Z/ } - users = users - user_ids + User.where(id: user_ids) + emails, users, existing_members = parse_users_list(source, users) self.transaction do - users.map do |user| + (emails + users).map! do |user| add_user( source, user, access_level, + existing_members: existing_members, current_user: current_user, expires_at: expires_at ) @@ -189,6 +178,31 @@ class Member < ActiveRecord::Base private + def parse_users_list(source, list) + emails, user_ids, users = [], [], [] + existing_members = {} + + list.each do |item| + case item + when User + users << item + when Integer + user_ids << item + when /\A\d+\Z/ + user_ids << item.to_i + when Devise.email_regexp + emails << item + end + end + + if user_ids.present? + users.concat(User.where(id: user_ids)) + existing_members = source.members_and_requesters.where(user_id: user_ids).index_by(&:user_id) + end + + [emails, users, existing_members] + end + # This method is used to find users that have been entered into the "Add members" field. # These can be the User objects directly, their IDs, their emails, or new emails to be invited. def retrieve_user(user) @@ -197,6 +211,20 @@ class Member < ActiveRecord::Base User.find_by(id: user) || User.find_by(email: user) || user end + def retrieve_member(source, user, existing_members) + user = retrieve_user(user) + + if user.is_a?(User) + if existing_members + existing_members[user.id] || source.members.build(user_id: user.id) + else + source.members_and_requesters.find_or_initialize_by(user_id: user.id) + end + else + source.members.build(invite_email: user) + end + end + def retrieve_access_level(access_level) access_levels.fetch(access_level) { access_level.to_i } end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7a817eedec2..b82f49d7073 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -918,6 +918,12 @@ class MergeRequest < ActiveRecord::Base active_diff_discussions.each do |discussion| service.execute(discussion) end + + if project.resolve_outdated_diff_discussions? + MergeRequests::ResolvedDiscussionNotificationService + .new(project, current_user) + .execute(self) + end end def keep_around_commit @@ -957,13 +963,6 @@ class MergeRequest < ActiveRecord::Base private def write_ref - target_project.repository.with_repo_branch_commit( - source_project.repository, source_branch) do |commit| - if commit - target_project.repository.write_ref(ref_path, commit.sha) - else - raise Rugged::ReferenceError, 'source repository is empty' - end - end + target_project.repository.fetch_source_branch(source_project.repository, source_branch, ref_path) end end diff --git a/app/models/project.rb b/app/models/project.rb index b9247fb535a..fdd516ec2ae 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -37,6 +37,7 @@ class Project < ActiveRecord::Base default_value_for :archived, false default_value_for :visibility_level, gitlab_config_features.visibility_level + default_value_for :resolve_outdated_diff_discussions, false default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for(:repository_storage) { current_application_settings.pick_repository_storage } default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } @@ -68,7 +69,6 @@ class Project < ActiveRecord::Base acts_as_taggable - attr_accessor :new_default_branch attr_accessor :old_path_with_namespace attr_accessor :template_name attr_writer :pipeline_status @@ -145,6 +145,7 @@ class Project < ActiveRecord::Base has_many :requesters, -> { where.not(requested_at: nil) }, as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent + has_many :members_and_requesters, as: :source, class_name: 'ProjectMember' has_many :deploy_keys_projects has_many :deploy_keys, through: :deploy_keys_projects diff --git a/app/models/repository.rb b/app/models/repository.rb index 5474c8eeb68..035f85a0b46 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -20,7 +20,6 @@ class Repository delegate :ref_name_for_sha, to: :raw_repository - CommitError = Class.new(StandardError) CreateTreeError = Class.new(StandardError) # Methods that cache data from the Git repository. @@ -95,19 +94,6 @@ class Repository "#<#{self.class.name}:#{@disk_path}>" end - # - # Git repository can contains some hidden refs like: - # /refs/notes/* - # /refs/git-as-svn/* - # /refs/pulls/* - # This refs by default not visible in project page and not cloned to client side. - # - # This method return true if repository contains some content visible in project page. - # - def has_visible_content? - branch_count > 0 - end - def commit(ref = 'HEAD') return nil unless exists? @@ -180,32 +166,25 @@ class Repository end def add_branch(user, branch_name, ref) - newrev = commit(ref).try(:sha) - - return false unless newrev - - GitOperationService.new(user, self).add_branch(branch_name, newrev) + branch = raw_repository.add_branch(branch_name, committer: user, target: ref) after_create_branch - find_branch(branch_name) + + branch + rescue Gitlab::Git::Repository::InvalidRef + false end def add_tag(user, tag_name, target, message = nil) - newrev = commit(target).try(:id) - options = { message: message, tagger: user_to_committer(user) } if message - - return false unless newrev - - GitOperationService.new(user, self).add_tag(tag_name, newrev, options) - - find_tag(tag_name) + raw_repository.add_tag(tag_name, committer: user, target: target, message: message) + rescue Gitlab::Git::Repository::InvalidRef + false end def rm_branch(user, branch_name) before_remove_branch - branch = find_branch(branch_name) - GitOperationService.new(user, self).rm_branch(branch) + raw_repository.rm_branch(branch_name, committer: user) after_remove_branch true @@ -213,9 +192,8 @@ class Repository def rm_tag(user, tag_name) before_remove_tag - tag = find_tag(tag_name) - GitOperationService.new(user, self).rm_tag(tag) + raw_repository.rm_tag(tag_name, committer: user) after_remove_tag true @@ -784,16 +762,30 @@ class Repository multi_action(**options) end + def with_branch(user, *args) + result = Gitlab::Git::OperationService.new(user, raw_repository).with_branch(*args) do |start_commit| + yield start_commit + end + + newrev, should_run_after_create, should_run_after_create_branch = result + + after_create if should_run_after_create + after_create_branch if should_run_after_create_branch + + newrev + end + # rubocop:disable Metrics/ParameterLists def multi_action( user:, branch_name:, message:, actions:, author_email: nil, author_name: nil, start_branch_name: nil, start_project: project) - GitOperationService.new(user, self).with_branch( + with_branch( + user, branch_name, start_branch_name: start_branch_name, - start_project: start_project) do |start_commit| + start_repository: start_project.repository.raw_repository) do |start_commit| index = Gitlab::Git::Index.new(raw_repository) @@ -846,7 +838,8 @@ class Repository end def merge(user, source, merge_request, options = {}) - GitOperationService.new(user, self).with_branch( + with_branch( + user, merge_request.target_branch) do |start_commit| our_commit = start_commit.sha their_commit = source @@ -866,17 +859,18 @@ class Repository merge_request.update(in_progress_merge_commit_sha: commit_id) commit_id end - rescue Repository::CommitError # when merge_index.conflicts? + rescue Gitlab::Git::CommitError # when merge_index.conflicts? false end def revert( user, commit, branch_name, start_branch_name: nil, start_project: project) - GitOperationService.new(user, self).with_branch( + with_branch( + user, branch_name, start_branch_name: start_branch_name, - start_project: start_project) do |start_commit| + start_repository: start_project.repository.raw_repository) do |start_commit| revert_tree_id = check_revert_content(commit, start_commit.sha) unless revert_tree_id @@ -896,10 +890,11 @@ class Repository def cherry_pick( user, commit, branch_name, start_branch_name: nil, start_project: project) - GitOperationService.new(user, self).with_branch( + with_branch( + user, branch_name, start_branch_name: start_branch_name, - start_project: start_project) do |start_commit| + start_repository: start_project.repository.raw_repository) do |start_commit| cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha) unless cherry_pick_tree_id @@ -921,7 +916,7 @@ class Repository end def resolve_conflicts(user, branch_name, params) - GitOperationService.new(user, self).with_branch(branch_name) do + with_branch(user, branch_name) do committer = user_to_committer(user) create_commit(params.merge(author: committer, committer: committer)) @@ -1011,25 +1006,6 @@ class Repository run_git(args).first.lines.map(&:strip) end - def with_repo_branch_commit(start_repository, start_branch_name) - return yield nil if start_repository.empty_repo? - - if start_repository == self - yield commit(start_branch_name) - else - sha = start_repository.commit(start_branch_name).sha - - if branch_commit = commit(sha) - yield branch_commit - else - with_repo_tmp_commit( - start_repository, start_branch_name, sha) do |tmp_commit| - yield tmp_commit - end - end - end - end - def add_remote(name, url) raw_repository.remote_add(name, url) rescue Rugged::ConfigError @@ -1047,14 +1023,12 @@ class Repository gitlab_shell.fetch_remote(raw_repository, remote, forced: forced, no_tags: no_tags) end - def fetch_ref(source_path, source_ref, target_ref) - args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) - message, status = run_git(args) - - # Make sure ref was created, and raise Rugged::ReferenceError when not - raise Rugged::ReferenceError, message if status != 0 + def fetch_source_branch(source_repository, source_branch, local_ref) + raw_repository.fetch_source_branch(source_repository.raw_repository, source_branch, local_ref) + end - target_ref + def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:) + raw_repository.compare_source_branch(target_branch_name, source_repository.raw_repository, source_branch_name, straight: straight) end def create_ref(ref, ref_path) @@ -1135,12 +1109,6 @@ class Repository private - def run_git(args) - circuit_breaker.perform do - Gitlab::Popen.popen([Gitlab.config.git.bin_path, *args], path_to_repo) - end - end - def blob_data_at(sha, path) blob = blob_at(sha, path) return unless blob @@ -1236,16 +1204,4 @@ class Repository .commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset) .map { |c| commit(c) } end - - def with_repo_tmp_commit(start_repository, start_branch_name, sha) - tmp_ref = fetch_ref( - start_repository.path_to_repo, - "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", - "refs/tmp/#{SecureRandom.hex}/head" - ) - - yield commit(sha) - ensure - delete_refs(tmp_ref) if tmp_ref - end end diff --git a/app/models/user.rb b/app/models/user.rb index 68ec93a3ec5..c5b5f09722f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -644,11 +644,6 @@ class User < ActiveRecord::Base @personal_projects_count ||= personal_projects.count end - def projects_limit_percent - return 100 if projects_limit.zero? - (personal_projects.count.to_f / projects_limit) * 100 - end - def recent_push(project_ids = nil) # Get push events not earlier than 2 hours ago events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours) @@ -666,10 +661,6 @@ class User < ActiveRecord::Base end end - def projects_sorted_by_activity - authorized_projects.sorted_by_activity - end - def several_namespaces? owned_groups.any? || masters_groups.any? end @@ -1050,6 +1041,10 @@ class User < ActiveRecord::Base ensure_rss_token! end + def verified_email?(email) + self.email == email + end + protected # override, from Devise::Validatable diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb index c495c3f39bb..255475e1fe6 100644 --- a/app/presenters/ci/build_presenter.rb +++ b/app/presenters/ci/build_presenter.rb @@ -17,5 +17,16 @@ module Ci "Job is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}" end end + + def trigger_variables + return [] unless trigger_request + + @trigger_variables ||= + if pipeline.variables.any? + pipeline.variables.map(&:to_runner_variable) + else + trigger_request.user_variables + end + end end end diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb deleted file mode 100644 index b2aa457bbd5..00000000000 --- a/app/services/ci/create_trigger_request_service.rb +++ /dev/null @@ -1,19 +0,0 @@ -# This class is deprecated because we're closing Ci::TriggerRequest. -# New class is PipelineTriggerService (app/services/ci/pipeline_trigger_service.rb) -# which is integrated with Ci::PipelineVariable instaed of Ci::TriggerRequest. -# We remove this class after we removed v1 and v3 API. This class is still being -# referred by such legacy code. -module Ci - module CreateTriggerRequestService - Result = Struct.new(:trigger_request, :pipeline) - - def self.execute(project, trigger, ref, variables = nil) - trigger_request = trigger.trigger_requests.create(variables: variables) - - pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref) - .execute(:trigger, ignore_skip_ci: true, trigger_request: trigger_request) - - Result.new(trigger_request, pipeline) - end - end -end diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb index dbd0b9ef43a..f96f2931508 100644 --- a/app/services/commits/create_service.rb +++ b/app/services/commits/create_service.rb @@ -17,7 +17,7 @@ module Commits new_commit = create_commit! success(result: new_commit) - rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Repository::CommitError, Gitlab::Git::HooksService::PreReceiveError => ex + rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Gitlab::Git::CommitError, Gitlab::Git::HooksService::PreReceiveError => ex error(ex.message) end diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index a5ae4927412..53f16a236d2 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -11,26 +11,8 @@ class CompareService end def execute(target_project, target_branch, straight: false) - # If compare with other project we need to fetch ref first - target_project.repository.with_repo_branch_commit( - start_project.repository, - start_branch_name) do |commit| - break unless commit + raw_compare = target_project.repository.compare_source_branch(target_branch, start_project.repository, start_branch_name, straight: straight) - compare(commit.sha, target_project, target_branch, straight: straight) - end - end - - private - - def compare(source_sha, target_project, target_branch, straight:) - raw_compare = Gitlab::Git::Compare.new( - target_project.repository.raw_repository, - target_branch, - source_sha, - straight: straight - ) - - Compare.new(raw_compare, target_project, straight: straight) + Compare.new(raw_compare, target_project, straight: straight) if raw_compare end end diff --git a/app/services/discussions/update_diff_position_service.rb b/app/services/discussions/update_diff_position_service.rb index 1ef8d9edbe1..746f209e20f 100644 --- a/app/services/discussions/update_diff_position_service.rb +++ b/app/services/discussions/update_diff_position_service.rb @@ -10,6 +10,10 @@ module Discussions discussion.notes.each do |note| if outdated note.change_position = position + + if project.resolve_outdated_diff_discussions? + note.resolve_without_save(current_user, resolved_by_push: true) + end else note.position = position note.change_position = nil diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb deleted file mode 100644 index 6b7a56e6922..00000000000 --- a/app/services/git_operation_service.rb +++ /dev/null @@ -1,159 +0,0 @@ -class GitOperationService - attr_reader :committer, :repository - - def initialize(committer, new_repository) - committer = Gitlab::Git::Committer.from_user(committer) if committer.is_a?(User) - @committer = committer - - @repository = new_repository - end - - def add_branch(branch_name, newrev) - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name - oldrev = Gitlab::Git::BLANK_SHA - - update_ref_in_hooks(ref, newrev, oldrev) - end - - def rm_branch(branch) - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch.name - oldrev = branch.target - newrev = Gitlab::Git::BLANK_SHA - - update_ref_in_hooks(ref, newrev, oldrev) - end - - def add_tag(tag_name, newrev, options = {}) - ref = Gitlab::Git::TAG_REF_PREFIX + tag_name - oldrev = Gitlab::Git::BLANK_SHA - - with_hooks(ref, newrev, oldrev) do |service| - # We want to pass the OID of the tag object to the hooks. For an - # annotated tag we don't know that OID until after the tag object - # (raw_tag) is created in the repository. That is why we have to - # update the value after creating the tag object. Only the - # "post-receive" hook will receive the correct value in this case. - raw_tag = repository.rugged.tags.create(tag_name, newrev, options) - service.newrev = raw_tag.target_id - end - end - - def rm_tag(tag) - ref = Gitlab::Git::TAG_REF_PREFIX + tag.name - oldrev = tag.target - newrev = Gitlab::Git::BLANK_SHA - - update_ref_in_hooks(ref, newrev, oldrev) do - repository.rugged.tags.delete(tag_name) - end - end - - # Whenever `start_branch_name` is passed, if `branch_name` doesn't exist, - # it would be created from `start_branch_name`. - # If `start_project` is passed, and the branch doesn't exist, - # it would try to find the commits from it instead of current repository. - def with_branch( - branch_name, - start_branch_name: nil, - start_project: repository.project, - &block) - - start_repository = start_project.repository - start_branch_name = nil if start_repository.empty_repo? - - if start_branch_name && !start_repository.branch_exists?(start_branch_name) - raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.full_path}" - end - - update_branch_with_hooks(branch_name) do - repository.with_repo_branch_commit( - start_repository, - start_branch_name || branch_name, - &block) - end - end - - private - - def update_branch_with_hooks(branch_name) - update_autocrlf_option - - was_empty = repository.empty? - - # Make commit - newrev = yield - - unless newrev - raise Repository::CommitError.new('Failed to create commit') - end - - branch = repository.find_branch(branch_name) - oldrev = find_oldrev_from_branch(newrev, branch) - - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name - update_ref_in_hooks(ref, newrev, oldrev) - - # If repo was empty expire cache - repository.after_create if was_empty - repository.after_create_branch if - was_empty || Gitlab::Git.blank_ref?(oldrev) - - newrev - end - - def find_oldrev_from_branch(newrev, branch) - return Gitlab::Git::BLANK_SHA unless branch - - oldrev = branch.target - - if oldrev == repository.rugged.merge_base(newrev, branch.target) - oldrev - else - raise Repository::CommitError.new('Branch diverged') - end - end - - def update_ref_in_hooks(ref, newrev, oldrev) - with_hooks(ref, newrev, oldrev) do - update_ref(ref, newrev, oldrev) - end - end - - def with_hooks(ref, newrev, oldrev) - Gitlab::Git::HooksService.new.execute( - committer, - repository, - oldrev, - newrev, - ref) do |service| - - yield(service) - end - end - - # Gitaly note: JV: wait with migrating #update_ref until we know how to migrate its call sites. - def update_ref(ref, newrev, oldrev) - # We use 'git update-ref' because libgit2/rugged currently does not - # offer 'compare and swap' ref updates. Without compare-and-swap we can - # (and have!) accidentally reset the ref to an earlier state, clobbering - # commits. See also https://github.com/libgit2/libgit2/issues/1534. - command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z] - _, status = Gitlab::Popen.popen( - command, - repository.path_to_repo) do |stdin| - stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00") - end - - unless status.zero? - raise Repository::CommitError.new( - "Could not update branch #{Gitlab::Git.branch_name(ref)}." \ - " Please refresh and try again.") - end - end - - def update_autocrlf_option - if repository.raw_repository.autocrlf != :input - repository.raw_repository.autocrlf = :input - end - end -end diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index f6b83a2f621..d34903c9989 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -53,7 +53,7 @@ module Projects log_error("Projects::UpdatePagesService: #{message}") @status.allow_failure = !latest? @status.description = message - @status.drop + @status.drop(:script_failure) super end diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml index c91602fcff7..30bf1384b22 100644 --- a/app/views/ci/lints/_create.html.haml +++ b/app/views/ci/lints/_create.html.haml @@ -22,10 +22,10 @@ %b Tag list: = build[:tag_list].to_a.join(", ") %br - %b Refs only: + %b Only policy: = @jobs[build[:name].to_sym][:only].to_a.join(", ") %br - %b Refs except: + %b Except policy: = @jobs[build[:name].to_sym][:except].to_a.join(", ") %br %b Environment: diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml index 5a379eae8f4..11bf3f5d323 100644 --- a/app/views/dashboard/_groups_head.html.haml +++ b/app/views/dashboard/_groups_head.html.haml @@ -1,4 +1,4 @@ -- if show_new_nav? && current_user.can_create_group? +- if current_user.can_create_group? - content_for :breadcrumbs_extra do = link_to "New group", new_group_path, class: "btn btn-new" @@ -10,8 +10,8 @@ = nav_link(page: explore_groups_path) do = link_to explore_groups_path, title: 'Explore public groups' do Explore public groups - .nav-controls{ class: ("nav-controls-new-nav" if show_new_nav?) } + .nav-controls.nav-controls-new-nav = render 'shared/groups/search_form' = render 'shared/groups/dropdown' - if current_user.can_create_group? - = link_to "New group", new_group_path, class: "btn btn-new #{("visible-xs" if show_new_nav?)}" + = link_to "New group", new_group_path, class: "btn btn-new visible-xs" diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 1f9a5b401b6..e2a1914ada2 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -1,7 +1,7 @@ = content_for :flash_message do = render 'shared/project_limit' -- if show_new_nav? && current_user.can_create_project? +- if current_user.can_create_project? - content_for :breadcrumbs_extra do = link_to "New project", new_project_path, class: 'btn btn-new' @@ -19,8 +19,8 @@ = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do Explore projects - .nav-controls{ class: ("nav-controls-new-nav" if show_new_nav?) } + .nav-controls.nav-controls-new-nav = render 'shared/projects/search_form' = render 'shared/projects/dropdown' - if current_user.can_create_project? - = link_to "New project", new_project_path, class: "btn btn-new #{("visible-xs" if show_new_nav?)}" + = link_to "New project", new_project_path, class: "btn btn-new visible-xs" diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml index fd5389106bb..14c18678ab1 100644 --- a/app/views/dashboard/_snippets_head.html.haml +++ b/app/views/dashboard/_snippets_head.html.haml @@ -1,4 +1,4 @@ -- if show_new_nav? && current_user +- if current_user - content_for :breadcrumbs_extra do = link_to "New snippet", new_snippet_path, class: "btn btn-new", title: "New snippet" @@ -10,7 +10,3 @@ = nav_link(page: explore_snippets_path) do = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do Explore Snippets - - - if current_user - .nav-controls.hidden-xs{ class: ("hidden-sm hidden-md hidden-lg" if show_new_nav?) } - = link_to "New snippet", new_snippet_path, class: "btn btn-new", title: "New snippet" diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index 9ac44674b73..ad0e205a79f 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -4,15 +4,14 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues") -- if show_new_nav? - - content_for :breadcrumbs_extra do - = link_to params.merge(rss_url_options), class: 'btn has-tooltip append-right-10', title: 'Subscribe' do - = icon('rss') - = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues +- content_for :breadcrumbs_extra do + = link_to params.merge(rss_url_options), class: 'btn has-tooltip append-right-10', title: 'Subscribe' do + = icon('rss') + = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues .top-area = render 'shared/issuable/nav', type: :issues - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs = link_to params.merge(rss_url_options), class: 'btn has-tooltip', title: 'Subscribe' do = icon('rss') = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index 960e1e55f36..ccc74f7cf3d 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -2,13 +2,12 @@ - page_title "Merge Requests" - header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id) -- if show_new_nav? - - content_for :breadcrumbs_extra do - = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests +- content_for :breadcrumbs_extra do + = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests .top-area = render 'shared/issuable/nav', type: :merge_requests - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests = render 'shared/issuable/filter', type: :merge_requests diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index cb8bf57cba1..9fffdded1a0 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -2,14 +2,13 @@ - page_title 'Milestones' - header_title 'Milestones', dashboard_milestones_path -- if show_new_nav? - - content_for :breadcrumbs_extra do - = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New milestone', include_groups: true, type: :milestones +- content_for :breadcrumbs_extra do + = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New milestone', include_groups: true, type: :milestones .top-area = render 'shared/milestones_filter', counts: @milestone_states - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New milestone', include_groups: true, type: :milestones .milestones diff --git a/app/views/discussions/_headline.html.haml b/app/views/discussions/_headline.html.haml index 25e90924413..b865eb815f0 100644 --- a/app/views/discussions/_headline.html.haml +++ b/app/views/discussions/_headline.html.haml @@ -1,9 +1,11 @@ - if discussion.resolved? .discussion-headline-light.js-discussion-headline - Resolved + = discussion_resolved_intro(discussion) - if discussion.resolved_by by = link_to_member(@project, discussion.resolved_by, avatar: false) + - if discussion.resolved_by_push? + with a push = time_ago_with_tooltip(discussion.resolved_at, placement: "bottom") - elsif discussion.updated? .discussion-headline-light.js-discussion-headline diff --git a/app/views/feature_highlight/_issue_boards.svg b/app/views/feature_highlight/_issue_boards.svg new file mode 100644 index 00000000000..1522c9d51c9 --- /dev/null +++ b/app/views/feature_highlight/_issue_boards.svg @@ -0,0 +1,98 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="214" height="102" viewBox="0 0 214 102" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <path id="b" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,27 C48,28.1045695 47.1045695,29 46,29 L2,29 C0.8954305,29 1.3527075e-16,28.1045695 0,27 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> + <filter id="a" width="102.1%" height="106.9%" x="-1%" y="-1.7%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> + </filter> + <path id="d" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> + <filter id="c" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> + </filter> + <path id="e" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/> + <path id="h" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> + <filter id="g" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> + </filter> + <path id="j" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> + <filter id="i" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> + </filter> + <path id="l" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> + <filter id="k" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> + </filter> + <path id="n" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> + <filter id="m" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> + </filter> + <path id="p" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> + <filter id="o" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd"> + <path fill="#D6D4DE" d="M14,21 L62,21 C64.7614237,21 67,23.2385763 67,26 L67,112 C67,114.761424 64.7614237,117 62,117 L14,117 C11.2385763,117 9,114.761424 9,112 L9,26 C9,23.2385763 11.2385763,21 14,21 Z"/> + <g transform="translate(11 23)"> + <path fill="#FFFFFF" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/> + <path fill="#FC6D26" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z"/> + <g transform="translate(5 10)"> + <use fill="black" filter="url(#a)" xlink:href="#b"/> + <use fill="#F9F9F9" xlink:href="#b"/> + </g> + <g transform="translate(5 42)"> + <use fill="black" filter="url(#c)" xlink:href="#d"/> + <use fill="#FEF0E8" xlink:href="#d"/> + <path fill="#FEE1D3" d="M9,8 L33,8 C34.1045695,8 35,8.8954305 35,10 C35,11.1045695 34.1045695,12 33,12 L9,12 C7.8954305,12 7,11.1045695 7,10 C7,8.8954305 7.8954305,8 9,8 Z"/> + <path fill="#FDC4A8" d="M9,17 L17,17 C18.1045695,17 19,17.8954305 19,19 C19,20.1045695 18.1045695,21 17,21 L9,21 C7.8954305,21 7,20.1045695 7,19 C7,17.8954305 7.8954305,17 9,17 Z"/> + <path fill="#FC6D26" d="M24,17 L32,17 C33.1045695,17 34,17.8954305 34,19 C34,20.1045695 33.1045695,21 32,21 L24,21 C22.8954305,21 22,20.1045695 22,19 C22,17.8954305 22.8954305,17 24,17 Z"/> + </g> + </g> + <path fill="#D6D4DE" d="M148,26 L196,26 C198.761424,26 201,28.2385763 201,31 L201,117 C201,119.761424 198.761424,122 196,122 L148,122 C145.238576,122 143,119.761424 143,117 L143,31 C143,28.2385763 145.238576,26 148,26 Z"/> + <g transform="translate(145 28)"> + <mask id="f" fill="white"> + <use xlink:href="#e"/> + </mask> + <use fill="#FFFFFF" xlink:href="#e"/> + <path fill="#FC6D26" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z" mask="url(#f)"/> + <g transform="translate(5 10)"> + <use fill="black" filter="url(#g)" xlink:href="#h"/> + <use fill="#F9F9F9" xlink:href="#h"/> + </g> + <g transform="translate(5 42)"> + <use fill="black" filter="url(#i)" xlink:href="#j"/> + <use fill="#FEF0E8" xlink:href="#j"/> + <path fill="#FEE1D3" d="M9 8L33 8C34.1045695 8 35 8.8954305 35 10 35 11.1045695 34.1045695 12 33 12L9 12C7.8954305 12 7 11.1045695 7 10 7 8.8954305 7.8954305 8 9 8zM9 17L13 17C14.1045695 17 15 17.8954305 15 19 15 20.1045695 14.1045695 21 13 21L9 21C7.8954305 21 7 20.1045695 7 19 7 17.8954305 7.8954305 17 9 17z"/> + <path fill="#FC6D26" d="M20,17 L24,17 C25.1045695,17 26,17.8954305 26,19 C26,20.1045695 25.1045695,21 24,21 L20,21 C18.8954305,21 18,20.1045695 18,19 C18,17.8954305 18.8954305,17 20,17 Z"/> + <path fill="#FDC4A8" d="M31,17 L35,17 C36.1045695,17 37,17.8954305 37,19 C37,20.1045695 36.1045695,21 35,21 L31,21 C29.8954305,21 29,20.1045695 29,19 C29,17.8954305 29.8954305,17 31,17 Z"/> + </g> + </g> + <path fill="#D6D4DE" d="M81,14 L129,14 C131.761424,14 134,16.2385763 134,19 L134,105 C134,107.761424 131.761424,110 129,110 L81,110 C78.2385763,110 76,107.761424 76,105 L76,19 C76,16.2385763 78.2385763,14 81,14 Z"/> + <g transform="translate(78 16)"> + <path fill="#FFFFFF" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/> + <g transform="translate(5 10)"> + <use fill="black" filter="url(#k)" xlink:href="#l"/> + <use fill="#EFEDF8" xlink:href="#l"/> + <path fill="#E1DBF1" d="M9,8 L33,8 C34.1045695,8 35,8.8954305 35,10 C35,11.1045695 34.1045695,12 33,12 L9,12 C7.8954305,12 7,11.1045695 7,10 C7,8.8954305 7.8954305,8 9,8 Z"/> + <path fill="#6B4FBB" d="M9,17 L13,17 C14.1045695,17 15,17.8954305 15,19 C15,20.1045695 14.1045695,21 13,21 L9,21 C7.8954305,21 7,20.1045695 7,19 C7,17.8954305 7.8954305,17 9,17 Z"/> + <path fill="#C3B8E3" d="M20,17 L28,17 C29.1045695,17 30,17.8954305 30,19 C30,20.1045695 29.1045695,21 28,21 L20,21 C18.8954305,21 18,20.1045695 18,19 C18,17.8954305 18.8954305,17 20,17 Z"/> + </g> + <g transform="translate(5 42)"> + <use fill="black" filter="url(#m)" xlink:href="#n"/> + <use fill="#F9F9F9" xlink:href="#n"/> + </g> + <g transform="translate(5 74)"> + <rect width="34" height="4" x="7" y="7" fill="#E1DBF1" rx="2"/> + <use fill="black" filter="url(#o)" xlink:href="#p"/> + <use fill="#F9F9F9" xlink:href="#p"/> + </g> + <path fill="#6B4FBB" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z"/> + </g> + </g> +</svg> diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 837ef385dd5..13a4b4c90c9 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -8,7 +8,7 @@ = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'filtered_search' -- if show_new_nav? && group_issues_exists +- if group_issues_exists - content_for :breadcrumbs_extra do = link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10' do = icon('rss') @@ -19,7 +19,7 @@ - if group_issues_exists .top-area = render 'shared/issuable/nav', type: :issues - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs = link_to params.merge(rss_url_options), class: 'btn' do = icon('rss') %span.icon-label diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index 50179a47797..9e59a09d459 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -1,5 +1,5 @@ - page_title 'Labels' -- if show_new_nav? && can?(current_user, :admin_label, @group) +- if can?(current_user, :admin_label, @group) - content_for :breadcrumbs_extra do = link_to "New label", new_group_label_path(@group), class: "btn btn-new" @@ -10,7 +10,7 @@ .nav-text Labels can be applied to issues and merge requests. Group labels are available for any project within the group. - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs - if can?(current_user, :admin_label, @group) = link_to "New label", new_group_label_path(@group), class: "btn btn-new" diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index 184df6f5406..0344770e0dd 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -4,7 +4,7 @@ = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'filtered_search' -- if show_new_nav? && current_user +- if current_user - content_for :breadcrumbs_extra do = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests @@ -14,7 +14,7 @@ .top-area = render 'shared/issuable/nav', type: :merge_requests - if current_user - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests = render 'shared/issuable/search_bar', type: :merge_requests diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index 66c6cc9e279..6e7a1af243d 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -1,5 +1,5 @@ - page_title "Milestones" -- if show_new_nav? && can?(current_user, :admin_milestones, @group) +- if can?(current_user, :admin_milestones, @group) - content_for :breadcrumbs_extra do = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new" @@ -8,7 +8,7 @@ .top-area = render 'shared/milestones_filter', counts: @milestone_states - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs - if can?(current_user, :admin_milestones, @group) = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new" diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 3babdae3968..34e85fef6d9 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -32,9 +32,9 @@ = stylesheet_link_tag "test", media: "all" if Rails.env.test? = stylesheet_link_tag 'performance_bar' if performance_bar_enabled? - - if show_new_nav? - = stylesheet_link_tag "new_nav", media: "all" - = stylesheet_link_tag "new_sidebar", media: "all" + // TODO: Combine these 2 stylesheets into application.scss + = stylesheet_link_tag "new_nav", media: "all" + = stylesheet_link_tag "new_sidebar", media: "all" = Gon::Base.render_data diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index c4f8cd71395..1fd301d6850 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,26 +1,14 @@ .page-with-sidebar{ class: page_with_sidebar_class } - - if show_new_nav? - - if defined?(nav) && nav - = render "layouts/nav/#{nav}" - - else - - if defined?(nav) && nav - .layout-nav - .container-fluid - = render "layouts/nav/#{nav}" - - if content_for?(:sub_nav) - = yield :sub_nav - .content-wrapper{ class: layout_nav_class } - - if show_new_nav? - .mobile-overlay + - if defined?(nav) && nav + = render "layouts/nav/sidebar/#{nav}" + .content-wrapper.page-with-new-nav + .mobile-overlay .alert-wrapper = render "layouts/broadcast" - - if show_new_nav? - - if content_for?(:new_global_flash) - = yield :new_global_flash - - unless @hide_breadcrumbs - = render "layouts/nav/breadcrumbs" - = render "layouts/flash" = yield :flash_message + - unless @hide_breadcrumbs + = render "layouts/nav/breadcrumbs" + = render "layouts/flash" %div{ class: "#{(container_class unless @no_container)} #{@content_class}" } .content{ id: "content-body" } = yield diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index ae9eee215e0..8595157a997 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -1,9 +1,6 @@ - page_title "Admin Area" - header_title "Admin Area", admin_root_path -- if show_new_nav? - - nav "new_admin_sidebar" - - @new_sidebar = true -- else - - nav "admin" +- nav "admin" +- @left_sidebar = true = render template: "layouts/application" diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index b53f382fa3d..65ac8aaa59b 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -4,10 +4,7 @@ %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } } = render "layouts/init_auto_complete" if @gfm_form = render 'peek/bar' - - if show_new_nav? - = render "layouts/header/new" - - else - = render "layouts/header/default", title: header_title + = render "layouts/header/default" = render 'layouts/page', sidebar: sidebar, nav: nav = yield :scripts_body diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index 35abfa0e80c..08bd6fc311e 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -1,10 +1,7 @@ - page_title @group.name - page_description @group.description unless page_description - header_title group_title(@group) unless header_title -- if show_new_nav? - - nav "new_group_sidebar" - - @new_sidebar = true -- else - - nav "group" +- nav "group" +- @left_sidebar = true = render template: "layouts/application" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 1d875f81041..d8fc371497d 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -1,68 +1,50 @@ -%header.navbar.navbar-gitlab{ class: nav_header_class } - .navbar-border +%header.navbar.navbar-gitlab.navbar-gitlab-new %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content .container-fluid .header-content - .dropdown.global-dropdown - %button.global-dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } - %span.sr-only Toggle navigation - = icon('bars') - .dropdown-menu-nav.global-dropdown-menu - - if current_user - = render 'layouts/nav/dashboard' - - else - = render 'layouts/nav/explore' + .title-container + %h1.title + = link_to root_path, title: 'Dashboard', id: 'logo' do + = brand_header_logo + %span.logo-text.hidden-xs + = render 'shared/logo_type.svg' - .header-logo - = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do - = brand_header_logo - - .title-container.js-title-container - %h1.title{ class: ('initializing' if @has_group_title) }= title + - if current_user + = render "layouts/nav/dashboard" + - else + = render "layouts/nav/explore" .navbar-collapse.collapse %ul.nav.navbar-nav + - if current_user + = render 'layouts/header/new_dropdown' %li.hidden-sm.hidden-xs = render 'layouts/search' unless current_controller?(:search) %li.visible-sm-inline-block.visible-xs-inline-block = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('search') - if current_user - - if session[:impersonator_id] - %li.impersonation - = link_to admin_impersonation_path, method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do - = icon('user-secret fw') - - if current_user.admin? - %li - = link_to admin_root_path, title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('wrench fw') - = render 'layouts/header/new_dropdown' - - if Gitlab::Sherlock.enabled? - %li - = link_to sherlock_transactions_path, title: 'Sherlock Transactions', - data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('tachometer fw') - %li - = link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + %li.user-counter + = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = custom_icon('issues') - issues_count = assigned_issuables_count(:issues) %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) } = number_with_delimiter(issues_count) - %li - = link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + %li.user-counter + = link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = custom_icon('mr_bold') - merge_requests_count = assigned_issuables_count(:merge_requests) %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) } = number_with_delimiter(merge_requests_count) - %li + %li.user-counter = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('check-circle fw') + = custom_icon('todo_done') %span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) } = todos_count_format(todos_pending_count) %li.header-user.dropdown - = link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do - = image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar" - = icon('caret-down') + = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do + = image_tag avatar_icon(current_user, 23), width: 23, height: 23, class: "header-user-avatar" + = custom_icon('caret_down') .dropdown-menu-nav.dropdown-menu-align-right %ul %li.current-user @@ -74,18 +56,24 @@ = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username } %li = link_to "Settings", profile_path + - if current_user + %li + = link_to "Help", help_path %li.divider %li = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link" + - if session[:impersonator_id] + %li.impersonation + = link_to admin_impersonation_path, class: 'impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do + = icon('user-secret') - else %li %div - = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success' + = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in' - %button.navbar-toggle{ type: 'button' } + %button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' } %span.sr-only Toggle navigation - = icon('ellipsis-v') - - = yield :header_content + = icon('ellipsis-v', class: 'js-navbar-toggle-right') + = icon('times', class: 'js-navbar-toggle-left') = render 'shared/outdated_browser' diff --git a/app/views/layouts/header/_new.html.haml b/app/views/layouts/header/_new.html.haml deleted file mode 100644 index c84d7053cd6..00000000000 --- a/app/views/layouts/header/_new.html.haml +++ /dev/null @@ -1,84 +0,0 @@ -%header.navbar.navbar-gitlab.navbar-gitlab-new{ class: nav_header_class } - %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content - .container-fluid - .header-content - .title-container - %h1.title - = link_to root_path, title: 'Dashboard', id: 'logo' do - = brand_header_logo - %span.logo-text.hidden-xs - = render 'shared/logo_type.svg' - - - if current_user - = render "layouts/nav/new_dashboard" - - else - = render "layouts/nav/new_explore" - - .navbar-collapse.collapse - %ul.nav.navbar-nav - %li.hidden-sm.hidden-xs - = render 'layouts/search' unless current_controller?(:search) - %li.visible-sm-inline-block.visible-xs-inline-block - = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('search') - - if current_user - - if session[:impersonator_id] - %li.impersonation - = link_to admin_impersonation_path, method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do - = icon('user-secret fw') - - if current_user.admin? - %li - = link_to admin_root_path, title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('wrench fw') - = render 'layouts/header/new_dropdown' - - if Gitlab::Sherlock.enabled? - %li - = link_to sherlock_transactions_path, title: 'Sherlock Transactions', - data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('tachometer fw') - %li - = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = custom_icon('issues') - - issues_count = assigned_issuables_count(:issues) - %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) } - = number_with_delimiter(issues_count) - %li - = link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = custom_icon('mr_bold') - - merge_requests_count = assigned_issuables_count(:merge_requests) - %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) } - = number_with_delimiter(merge_requests_count) - %li - = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('check-circle fw') - %span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) } - = todos_count_format(todos_pending_count) - %li.header-user.dropdown - = link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do - = image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar" - = icon('chevron-down') - .dropdown-menu-nav.dropdown-menu-align-right - %ul - %li.current-user - .user-name.bold - = current_user.name - @#{current_user.username} - %li.divider - %li - = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username } - %li - = link_to "Settings", profile_path - %li.divider - %li - = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link" - - else - %li - %div - = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success' - - %button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' } - %span.sr-only Toggle navigation - = icon('ellipsis-v', class: 'js-navbar-toggle-right') - = icon('times', class: 'js-navbar-toggle-left') - -= render 'shared/outdated_browser' diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index 9da739b0974..63d1c077ecd 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -1,11 +1,7 @@ %li.header-new.dropdown = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do - - if show_new_nav? - = icon('plus') - = icon('chevron-down') - - else - = icon('plus fw') - = icon('caret-down') + = custom_icon('plus_square') + = custom_icon('caret_down') .dropdown-menu-nav.dropdown-menu-align-right %ul - if @group&.persisted? diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml deleted file mode 100644 index 6df0adfd742..00000000000 --- a/app/views/layouts/nav/_admin.html.haml +++ /dev/null @@ -1,40 +0,0 @@ -= render 'layouts/nav/admin_settings' -.scrolling-tabs-container{ class: nav_control_class } - .fade-left - = icon('angle-left') - .fade-right - = icon('angle-right') - %ul.nav-links.scrolling-tabs - = nav_link(controller: %w(dashboard admin projects users groups builds runners cohorts), html_options: {class: 'home'}) do - = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do - %span - Overview - = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do - = link_to admin_conversational_development_index_path, title: 'Monitoring' do - %span - Monitoring - = nav_link(controller: :broadcast_messages) do - = link_to admin_broadcast_messages_path, title: 'Messages' do - %span - Messages - = nav_link(controller: [:hooks, :hook_logs]) do - = link_to admin_hooks_path, title: 'Hooks' do - %span - System Hooks - - = nav_link(controller: :applications) do - = link_to admin_applications_path, title: 'Applications' do - %span - Applications - - = nav_link(controller: :abuse_reports) do - = link_to admin_abuse_reports_path, title: "Abuse Reports" do - %span - Abuse Reports - %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) - - - if akismet_enabled? - = nav_link(controller: :spam_logs) do - = link_to admin_spam_logs_path, title: "Spam Logs" do - %span - Spam Logs diff --git a/app/views/layouts/nav/_admin_settings.html.haml b/app/views/layouts/nav/_admin_settings.html.haml deleted file mode 100644 index 9de0e12a826..00000000000 --- a/app/views/layouts/nav/_admin_settings.html.haml +++ /dev/null @@ -1,31 +0,0 @@ -.controls - .dropdown.admin-settings-dropdown - %a.dropdown-new.btn.btn-default{ href: '#', 'data-toggle' => 'dropdown' } - = icon('cog') - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right - = nav_link(controller: :deploy_keys) do - = link_to admin_deploy_keys_path, title: 'Deploy Keys' do - %span - Deploy Keys - - = nav_link(controller: :services) do - = link_to admin_application_settings_services_path, title: 'Service Templates' do - %span - Service Templates - - = nav_link(controller: :labels) do - = link_to admin_labels_path, title: 'Labels' do - %span - Labels - - = nav_link(controller: :appearances) do - = link_to admin_appearances_path, title: 'Appearances' do - %span - Appearance - - %li.divider - = nav_link(controller: :application_settings) do - = link_to admin_application_settings_path, title: 'Settings' do - %span - Settings diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml index 653452871a0..ff0f6489861 100644 --- a/app/views/layouts/nav/_breadcrumbs.html.haml +++ b/app/views/layouts/nav/_breadcrumbs.html.haml @@ -4,7 +4,7 @@ %nav.breadcrumbs{ role: "navigation" } .breadcrumbs-container{ class: [container, @content_class] } - - if defined?(@new_sidebar) + - if defined?(@left_sidebar) = button_tag class: 'toggle-mobile-nav', type: 'button' do %span.sr-only Open sidebar = icon ('bars') diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index be7d27df2a0..8a39c4d775f 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,67 +1,62 @@ -%ul - = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do - = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - P - %span - Projects - = nav_link(path: 'dashboard#activity') do - = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - A - %span - Activity - - if koding_enabled? - = nav_link(controller: :koding) do - = link_to koding_path, title: 'Koding' do - %span - Koding - = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do +%ul.list-unstyled.navbar-sub-nav + = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown" }) do + %a{ href: "#", data: { toggle: "dropdown" } } + Projects + = custom_icon('caret_down') + .dropdown-menu.projects-dropdown-menu + = render "layouts/nav/projects_dropdown/show" + + = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "hidden-xs" }) do = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - G - %span - Groups - = nav_link(controller: 'dashboard/milestones') do + Groups + + = nav_link(path: 'dashboard#activity', html_options: { class: "visible-lg" }) do + = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do + Activity + + = nav_link(controller: 'dashboard/milestones', html_options: { class: "visible-lg" }) do = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - L - %span - Milestones - = nav_link(path: 'dashboard#issues') do - = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - I - %span.badge.pull-right= number_with_delimiter(assigned_issuables_count(:issues)) - %span - Issues - = nav_link(path: 'dashboard#merge_requests') do - = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - M - %span.badge.pull-right= number_with_delimiter(assigned_issuables_count(:merge_requests)) - %span - Merge Requests - = nav_link(controller: 'dashboard/snippets') do + Milestones + + = nav_link(controller: 'dashboard/snippets', html_options: { class: "visible-lg" }) do = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - S - %span - Snippets - %li.divider - %li - = link_to "Help", help_path, title: 'About GitLab CE', class: 'about-gitlab' + Snippets + + %li.dropdown.hidden-lg + %a{ href: "#", data: { toggle: "dropdown" } } + More + = custom_icon('caret_down') + .dropdown-menu + %ul + = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "visible-xs" }) do + = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do + Groups + + = nav_link(path: 'dashboard#activity') do + = link_to activity_dashboard_path, title: 'Activity' do + Activity + + = nav_link(controller: 'dashboard/milestones') do + = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do + Milestones + + = nav_link(controller: 'dashboard/snippets') do + = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do + Snippets + + -# Shortcut to Dashboard > Projects + %li.hidden + = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do + Projects + + - if current_user.admin? || Gitlab::Sherlock.enabled? + %li.line-separator.hidden-xs + - if current_user.admin? + = nav_link(controller: 'admin/dashboard') do + = link_to admin_root_path, class: 'admin-icon', title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('wrench fw') + - if Gitlab::Sherlock.enabled? + %li + = link_to sherlock_transactions_path, class: 'admin-icon', title: 'Sherlock Transactions', + data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('tachometer fw') diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml index 0cb367452f7..cd1c39f3226 100644 --- a/app/views/layouts/nav/_explore.html.haml +++ b/app/views/layouts/nav/_explore.html.haml @@ -1,30 +1,12 @@ -%ul +%ul.list-unstyled.navbar-sub-nav = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do = link_to explore_root_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - P - %span - Projects + Projects = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - G - %span - Groups + Groups = nav_link(controller: :snippets) do = link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - S - %span - Snippets - %li.divider - = nav_link(controller: :help) do - = link_to help_path, title: 'Help' do - %span - Help + Snippets + %li + = link_to "Help", help_path, title: 'About GitLab CE' diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml deleted file mode 100644 index 261445ecd2b..00000000000 --- a/app/views/layouts/nav/_group.html.haml +++ /dev/null @@ -1,31 +0,0 @@ -.scrolling-tabs-container{ class: nav_control_class } - .fade-left - = icon('angle-left') - .fade-right - = icon('angle-right') - %ul.nav-links.scrolling-tabs - = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do - = link_to group_path(@group), title: 'Home' do - %span - Group - = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do - = link_to issues_group_path(@group), title: 'Issues' do - %span - Issues - - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - %span.badge.count= number_with_delimiter(issues.count) - = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group), title: 'Merge Requests' do - %span - Merge Requests - - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute - %span.badge.count= number_with_delimiter(merge_requests.count) - = nav_link(path: 'group_members#index') do - = link_to group_group_members_path(@group), title: 'Members' do - %span - Members - - if current_user && can?(current_user, :admin_group, @group) - = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do - = link_to edit_group_path(@group), title: 'Settings' do - %span - Settings diff --git a/app/views/layouts/nav/_new_dashboard.html.haml b/app/views/layouts/nav/_new_dashboard.html.haml deleted file mode 100644 index cfdfcbebc9f..00000000000 --- a/app/views/layouts/nav/_new_dashboard.html.haml +++ /dev/null @@ -1,33 +0,0 @@ -%ul.list-unstyled.navbar-sub-nav - = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "home"}) do - = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do - Projects - - = nav_link(controller: ['dashboard/groups', 'explore/groups']) do - = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do - Groups - - = nav_link(path: 'dashboard#activity', html_options: { class: "hidden-xs hidden-sm" }) do - = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do - Activity - - %li.dropdown - %a{ href: "#", data: { toggle: "dropdown" } } - More - = icon("chevron-down", class: "dropdown-chevron") - .dropdown-menu - %ul - = nav_link(path: 'dashboard#activity', html_options: { class: "visible-xs visible-sm" }) do - = link_to activity_dashboard_path, title: 'Activity' do - Activity - - = nav_link(controller: 'dashboard/milestones') do - = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do - Milestones - - = nav_link(controller: 'dashboard/snippets') do - = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do - Snippets - %li.divider - %li - = link_to "Help", help_path, title: 'About GitLab CE' diff --git a/app/views/layouts/nav/_new_explore.html.haml b/app/views/layouts/nav/_new_explore.html.haml deleted file mode 100644 index 40385f251e3..00000000000 --- a/app/views/layouts/nav/_new_explore.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -%ul.list-unstyled.navbar-sub-nav - = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do - = link_to explore_root_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do - Projects - = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do - = link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do - Groups - %li.dropdown - %a{ href: "#", data: { toggle: "dropdown" } } - More - = icon("chevron-down", class: "dropdown-chevron") - .dropdown-menu - %ul - = nav_link(controller: :snippets) do - = link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do - Snippets - %li.divider - %li - = link_to "Help", help_path, title: 'About GitLab CE' diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml deleted file mode 100644 index 448f6abedf2..00000000000 --- a/app/views/layouts/nav/_profile.html.haml +++ /dev/null @@ -1,57 +0,0 @@ -.scrolling-tabs-container - .fade-left - = icon('angle-left') - .fade-right - = icon('angle-right') - %ul.nav-links.scrolling-tabs - = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do - = link_to profile_path, title: 'Profile Settings' do - %span - Profile - = nav_link(controller: [:accounts, :two_factor_auths]) do - = link_to profile_account_path, title: 'Account' do - %span - Account - - if current_application_settings.user_oauth_applications? - = nav_link(controller: 'oauth/applications') do - = link_to applications_profile_path, title: 'Applications' do - %span - Applications - = nav_link(controller: :chat_names) do - = link_to profile_chat_names_path, title: 'Chat' do - %span - Chat - = nav_link(controller: :personal_access_tokens) do - = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do - %span - Access Tokens - = nav_link(controller: :emails) do - = link_to profile_emails_path, title: 'Emails' do - %span - Emails - - unless current_user.ldap_user? - = nav_link(controller: :passwords) do - = link_to edit_profile_password_path, title: 'Password' do - %span - Password - = nav_link(controller: :notifications) do - = link_to profile_notifications_path, title: 'Notifications' do - %span - Notifications - - = nav_link(controller: :keys) do - = link_to profile_keys_path, title: 'SSH Keys' do - %span - SSH Keys - = nav_link(controller: :gpg_keys) do - = link_to profile_gpg_keys_path, title: 'GPG Keys' do - %span - GPG Keys - = nav_link(controller: :preferences) do - = link_to profile_preferences_path, title: 'Preferences' do - %span - Preferences - = nav_link(path: 'profiles#audit_log') do - = link_to audit_log_profile_path, title: 'Authentication log' do - %span - Authentication log diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml deleted file mode 100644 index b88465848e3..00000000000 --- a/app/views/layouts/nav/_project.html.haml +++ /dev/null @@ -1,111 +0,0 @@ -- can_edit = can?(current_user, :admin_project, @project) -.scrolling-tabs-container{ class: nav_control_class } - .fade-left - = icon('angle-left') - .fade-right - = icon('angle-right') - %ul.nav-links.scrolling-tabs - = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do - = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do - %span - Project - - - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do - = link_to project_tree_path(@project), title: 'Repository', class: 'shortcuts-tree' do - %span - Repository - - - if project_nav_tab? :container_registry - = nav_link(controller: %w[projects/registry/repositories]) do - = link_to project_container_registry_index_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do - %span - Registry - - - if project_nav_tab? :issues - = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do - = link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do - %span - Issues - - if @project.issues_enabled? - %span.badge.count.issue_counter - = number_with_delimiter(@project.open_issues_count) - - - if project_nav_tab? :merge_requests - - controllers = [:merge_requests, 'projects/merge_requests/conflicts'] - - controllers.push(:merge_requests, :labels, :milestones) unless @project.issues_enabled? - = nav_link(controller: controllers) do - = link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do - %span - Merge Requests - %span.badge.count.merge_counter.js-merge-counter - = number_with_delimiter(@project.open_merge_requests_count) - - - if project_nav_tab? :pipelines - = nav_link(controller: [:pipelines, :builds, :environments, :artifacts]) do - = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do - %span - Pipelines - - - if project_nav_tab? :wiki - = nav_link(controller: :wikis) do - = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do - %span - Wiki - - - if project_nav_tab? :snippets - = nav_link(controller: :snippets) do - = link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do - %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 - - -# Shortcut to Project > Activity - %li.hidden - = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do - %span - Activity - - -# Shortcut to Repository > Graph (formerly, Network) - - if project_nav_tab? :network - %li.hidden - = link_to project_network_path(@project, current_ref), title: 'Network', class: 'shortcuts-network' do - Graph - - -# Shortcut to Repository > Charts (formerly, top-nav item "Graphs") - - unless @project.empty_repo? - %li.hidden - = link_to charts_project_graph_path(@project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do - Charts - - -# Shortcut to Issues > New Issue - %li.hidden - = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do - Create a new issue - - -# Shortcut to Pipelines > Jobs - - if project_nav_tab? :builds - %li.hidden - = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do - Jobs - - -# Shortcut to commits page - - if project_nav_tab? :commits - %li.hidden - = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do - Commits - - -# Shortcut to issue boards - %li.hidden - = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards' diff --git a/app/views/layouts/nav/projects_dropdown/_show.html.haml b/app/views/layouts/nav/projects_dropdown/_show.html.haml new file mode 100644 index 00000000000..a7370180bf6 --- /dev/null +++ b/app/views/layouts/nav/projects_dropdown/_show.html.haml @@ -0,0 +1,15 @@ +- project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: @project.web_url, avatar_url: @project.avatar_url } if @project&.persisted? +.projects-dropdown-container + .project-dropdown-sidebar + %ul + = nav_link(path: 'dashboard/projects#index') do + = link_to dashboard_projects_path do + = _('Your projects') + = nav_link(path: 'projects#starred') do + = link_to starred_dashboard_projects_path do + = _('Starred projects') + = nav_link(path: 'projects#trending') do + = link_to explore_root_path do + = _('Explore projects') + .project-dropdown-content + #js-projects-dropdown{ data: { user_name: current_user.username, project: project_meta } } diff --git a/app/views/layouts/nav/_new_admin_sidebar.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index 3b53117deb6..3b53117deb6 100644 --- a/app/views/layouts/nav/_new_admin_sidebar.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml diff --git a/app/views/layouts/nav/_new_group_sidebar.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 5a1511b262f..5a1511b262f 100644 --- a/app/views/layouts/nav/_new_group_sidebar.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml diff --git a/app/views/layouts/nav/_new_profile_sidebar.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml index ccb6d1492f1..ccb6d1492f1 100644 --- a/app/views/layouts/nav/_new_profile_sidebar.html.haml +++ b/app/views/layouts/nav/sidebar/_profile.html.haml diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index f5361c7af0c..760c4c97c33 100644 --- a/app/views/layouts/nav/_new_project_sidebar.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -99,6 +99,20 @@ = link_to project_boards_path(@project), title: 'Board' do %span Board + .feature-highlight.js-feature-highlight{ disabled: true, data: { trigger: 'manual', container: 'body', toggle: 'popover', placement: 'right', highlight: 'issue-boards' } } + .feature-highlight-popover-content + = render 'feature_highlight/issue_boards.svg' + .feature-highlight-popover-sub-content + %span= _('Use') + = link_to 'Issue Boards', project_boards_path(@project) + %span= _('to create customized software development workflows like') + %strong= _('Scrum') + %span= _('or') + %strong= _('Kanban') + %hr + %button.btn-link.dismiss-feature-highlight{ type: 'button' } + %span= _("Got it! Don't show this again") + = custom_icon('thumbs_up') = nav_link(controller: :labels) do = link_to project_labels_path(@project), title: 'Labels' do diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml index c365839e605..67aa05b655c 100644 --- a/app/views/layouts/profile.html.haml +++ b/app/views/layouts/profile.html.haml @@ -1,10 +1,7 @@ - page_title "User Settings" - header_title "User Settings", profile_path unless header_title - sidebar "dashboard" -- if show_new_nav? - - nav "new_profile_sidebar" - - @new_sidebar = true -- else - - nav "profile" +- nav "profile" +- @left_sidebar = true = render template: "layouts/application" diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 54d56e9b873..6b847fb4b7c 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -1,11 +1,8 @@ - page_title @project.name_with_namespace - page_description @project.description unless page_description - header_title project_title(@project) unless header_title -- if show_new_nav? - - nav "new_project_sidebar" - - @new_sidebar = true -- else - - nav "project" +- nav "project" +- @left_sidebar = true - content_for :project_javascripts do - project = @target_project || @project @@ -14,12 +11,4 @@ :javascript window.uploads_path = "#{project_uploads_path(project)}"; -- content_for :header_content do - .js-dropdown-menu-projects - .dropdown-menu.dropdown-select.dropdown-menu-projects - = dropdown_title("Go to a project") - = dropdown_filter("Search your projects") - = dropdown_content - = dropdown_loading - = render template: "layouts/application" diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 33e062c1c9c..0b03276efcc 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -1,8 +1,5 @@ - page_title 'Two-Factor Authentication', 'Account' -- if show_new_nav? - - add_to_breadcrumbs("Account", profile_account_path) -- else - - header_title "Two-Factor Authentication", profile_two_factor_auth_path +- add_to_breadcrumbs("Account", profile_account_path) - @content_class = "limit-container-width" unless fluid_layout = render 'profiles/head' diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml index f47d84ef755..0175b519867 100644 --- a/app/views/projects/_flash_messages.html.haml +++ b/app/views/projects/_flash_messages.html.haml @@ -1,7 +1,6 @@ - project = local_assigns.fetch(:project) -- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message -= content_for flash_message_container do += content_for :flash_message do = render partial: 'deletion_failed', locals: { project: project } - if current_user && can?(current_user, :download_code, project) = render 'shared/no_ssh' diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml index 61420fd0fb6..e3effe45d6c 100644 --- a/app/views/projects/_merge_request_merge_settings.html.haml +++ b/app/views/projects/_merge_request_merge_settings.html.haml @@ -14,6 +14,10 @@ = form.check_box :only_allow_merge_if_all_discussions_are_resolved %strong Only allow merge requests to be merged if all discussions are resolved .checkbox + = form.label :resolve_outdated_diff_discussions do + = form.check_box :resolve_outdated_diff_discussions + %strong Automatically resolve merge request diff discussions when they become outdated + .checkbox = form.label :printing_merge_request_link_enabled do = form.check_box :printing_merge_request_link_enabled %strong Show link to create/view merge request when pushing from the command line diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml index 5452c6db6a6..2897c955c78 100644 --- a/app/views/projects/activity.html.haml +++ b/app/views/projects/activity.html.haml @@ -1,7 +1,5 @@ - @no_container = true - -- if show_new_nav? - - add_to_breadcrumbs(_("Project"), project_path(@project)) +- add_to_breadcrumbs(_("Project"), project_path(@project)) - page_title _("Activity") = render "projects/head" diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index 5354ec8522e..67d9db9aefe 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -2,9 +2,7 @@ - @no_container = true - @content_class = "issue-boards-content" - page_title "Boards" - -- if show_new_nav? - - add_to_breadcrumbs("Issues", project_issues_path(@project)) +- add_to_breadcrumbs("Issues", project_issues_path(@project)) - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' diff --git a/app/views/projects/boards/components/sidebar/_due_date.html.haml b/app/views/projects/boards/components/sidebar/_due_date.html.haml index f44a9d49a54..e8394eab213 100644 --- a/app/views/projects/boards/components/sidebar/_due_date.html.haml +++ b/app/views/projects/boards/components/sidebar/_due_date.html.haml @@ -3,7 +3,7 @@ Due date - if can?(current_user, :admin_issue, @project) = icon("spinner spin", class: "block-loading") - = link_to "Edit", "#", class: "edit-link pull-right" + = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right" .value .value-content %span.no-value{ "v-if" => "!issue.dueDate" } diff --git a/app/views/projects/boards/components/sidebar/_labels.html.haml b/app/views/projects/boards/components/sidebar/_labels.html.haml index 7d0c35fe183..6b389736e8b 100644 --- a/app/views/projects/boards/components/sidebar/_labels.html.haml +++ b/app/views/projects/boards/components/sidebar/_labels.html.haml @@ -3,7 +3,7 @@ Labels - if can?(current_user, :admin_issue, @project) = icon("spinner spin", class: "block-loading") - = link_to "Edit", "#", class: "edit-link pull-right" + = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right" .value.issuable-show-labels %span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" } None diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/projects/boards/components/sidebar/_milestone.html.haml index 002e9994ee0..a1ddb261ea3 100644 --- a/app/views/projects/boards/components/sidebar/_milestone.html.haml +++ b/app/views/projects/boards/components/sidebar/_milestone.html.haml @@ -3,7 +3,7 @@ Milestone - if can?(current_user, :admin_issue, @project) = icon("spinner spin", class: "block-loading") - = link_to "Edit", "#", class: "edit-link pull-right" + = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right" .value %span.no-value{ "v-if" => "!issue.milestone" } None diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 945a5c11d6d..b1834de0bad 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -1,9 +1,8 @@ - @no_container = true - page_title "Branches" -= render "projects/commits/head" +- add_to_breadcrumbs("Repository", project_tree_path(@project)) -- if show_new_nav? - - add_to_breadcrumbs("Repository", project_tree_path(@project)) += render "projects/commits/head" %div{ class: container_class } .top-area.adjust diff --git a/app/views/projects/commit/_invalid_signature_badge.html.haml b/app/views/projects/commit/_invalid_signature_badge.html.haml deleted file mode 100644 index 3a73aae9d95..00000000000 --- a/app/views/projects/commit/_invalid_signature_badge.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- title = capture do - .gpg-popover-icon.invalid - = render 'shared/icons/icon_status_notfound_borderless.svg' - %div - This commit was signed with an <strong>unverified</strong> signature. - -- locals = { signature: signature, title: title, label: 'Unverified', css_classes: ['invalid'] } - -= render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_other_user_signature_badge.html.haml b/app/views/projects/commit/_other_user_signature_badge.html.haml new file mode 100644 index 00000000000..80eca96f7ce --- /dev/null +++ b/app/views/projects/commit/_other_user_signature_badge.html.haml @@ -0,0 +1,6 @@ +- title = capture do + This commit was signed with a different user's verified signature. + +- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless', show_user: true } + += render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml b/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml new file mode 100644 index 00000000000..e737de48e22 --- /dev/null +++ b/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml @@ -0,0 +1,7 @@ +- title = capture do + This commit was signed with a verified signature, but the committer email + is <strong>not verified</strong> to belong to the same user. + +- locals = { signature: signature, title: title, label: 'Unverified', css_class: ['invalid'], icon: 'icon_status_notfound_borderless', show_user: true } + += render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_signature.html.haml b/app/views/projects/commit/_signature.html.haml index 60fa52557ef..145bc629380 100644 --- a/app/views/projects/commit/_signature.html.haml +++ b/app/views/projects/commit/_signature.html.haml @@ -1,5 +1,2 @@ - if signature - - if signature.valid_signature? - = render partial: 'projects/commit/valid_signature_badge', locals: { signature: signature } - - else - = render partial: 'projects/commit/invalid_signature_badge', locals: { signature: signature } + = render partial: "projects/commit/#{signature.verification_status}_signature_badge", locals: { signature: signature } diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml index d06b29db838..edff018ba6d 100644 --- a/app/views/projects/commit/_signature_badge.html.haml +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -1,17 +1,27 @@ -- css_classes = commit_signature_badge_classes(css_classes) +- signature = local_assigns.fetch(:signature) +- title = local_assigns.fetch(:title) +- label = local_assigns.fetch(:label) +- css_class = local_assigns.fetch(:css_class) +- icon = local_assigns.fetch(:icon) +- show_user = local_assigns.fetch(:show_user, false) + +- css_classes = commit_signature_badge_classes(css_class) - title = capture do .gpg-popover-status - = title + .gpg-popover-icon{ class: css_class } + = render "shared/icons/#{icon}.svg" + %div + = title - content = capture do - .clearfix - = content + - if show_user + .clearfix + = render partial: 'projects/commit/signature_badge_user', locals: { signature: signature } GPG Key ID: %span.monospace= signature.gpg_key_primary_keyid - = link_to('Learn more about signing commits', help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link') %button{ class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } } diff --git a/app/views/projects/commit/_signature_badge_user.html.haml b/app/views/projects/commit/_signature_badge_user.html.haml new file mode 100644 index 00000000000..b20198e76db --- /dev/null +++ b/app/views/projects/commit/_signature_badge_user.html.haml @@ -0,0 +1,21 @@ +- gpg_key = signature.gpg_key +- user = gpg_key&.user +- user_name = signature.gpg_key_user_name +- user_email = signature.gpg_key_user_email + +- if user + = link_to user_path(user), class: 'gpg-popover-user-link' do + %div + = user_avatar_without_link(user: user, size: 32) + + %div + %strong= user.name + %div= user.to_reference +- else + = mail_to user_email do + %div + = user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32) + + %div + %strong= user_name + %div= user_email diff --git a/app/views/projects/commit/_unknown_key_signature_badge.html.haml b/app/views/projects/commit/_unknown_key_signature_badge.html.haml new file mode 100644 index 00000000000..75c5cf57bcc --- /dev/null +++ b/app/views/projects/commit/_unknown_key_signature_badge.html.haml @@ -0,0 +1 @@ += render partial: 'projects/commit/unverified_signature_badge', locals: { signature: signature } diff --git a/app/views/projects/commit/_unverified_key_signature_badge.html.haml b/app/views/projects/commit/_unverified_key_signature_badge.html.haml new file mode 100644 index 00000000000..75c5cf57bcc --- /dev/null +++ b/app/views/projects/commit/_unverified_key_signature_badge.html.haml @@ -0,0 +1 @@ += render partial: 'projects/commit/unverified_signature_badge', locals: { signature: signature } diff --git a/app/views/projects/commit/_unverified_signature_badge.html.haml b/app/views/projects/commit/_unverified_signature_badge.html.haml new file mode 100644 index 00000000000..1af58027b83 --- /dev/null +++ b/app/views/projects/commit/_unverified_signature_badge.html.haml @@ -0,0 +1,6 @@ +- title = capture do + This commit was signed with an <strong>unverified</strong> signature. + +- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless' } + += render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_valid_signature_badge.html.haml b/app/views/projects/commit/_valid_signature_badge.html.haml deleted file mode 100644 index db1a41bbf64..00000000000 --- a/app/views/projects/commit/_valid_signature_badge.html.haml +++ /dev/null @@ -1,32 +0,0 @@ -- title = capture do - .gpg-popover-icon.valid - = render 'shared/icons/icon_status_success_borderless.svg' - %div - This commit was signed with a <strong>verified</strong> signature. - -- content = capture do - - gpg_key = signature.gpg_key - - user = gpg_key&.user - - user_name = signature.gpg_key_user_name - - user_email = signature.gpg_key_user_email - - - if user - = link_to user_path(user), class: 'gpg-popover-user-link' do - %div - = user_avatar_without_link(user: user, size: 32) - - %div - %strong= gpg_key.user.name - %div @#{gpg_key.user.username} - - else - = mail_to user_email do - %div - = user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32) - - %div - %strong= user_name - %div= user_email - -- locals = { signature: signature, title: title, content: content, label: 'Verified', css_classes: ['valid'] } - -= render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_verified_signature_badge.html.haml b/app/views/projects/commit/_verified_signature_badge.html.haml new file mode 100644 index 00000000000..423beba2120 --- /dev/null +++ b/app/views/projects/commit/_verified_signature_badge.html.haml @@ -0,0 +1,7 @@ +- title = capture do + This commit was signed with a <strong>verified</strong> signature and the + committer email is verified to belong to the same user. + +- locals = { signature: signature, title: title, label: 'Verified', css_class: 'valid', icon: 'icon_status_success_borderless', show_user: true } + += render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 7ae56086177..0f048079ef5 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -5,8 +5,7 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits") -- if show_new_nav? - - add_to_breadcrumbs("Repository", project_tree_path(@project)) +- add_to_breadcrumbs("Repository", project_tree_path(@project)) = content_for :sub_nav do = render "head" diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index 05de21e8dbf..ecef91855af 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -1,7 +1,6 @@ - @no_container = true - page_title "Compare" -- if show_new_nav? - - add_to_breadcrumbs("Repository", project_tree_path(@project)) +- add_to_breadcrumbs("Repository", project_tree_path(@project)) = render "projects/commits/head" %div{ class: container_class } diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index 8bc863f77b3..eca733a933a 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -1,8 +1,7 @@ - @no_container = true - breadcrumb_title "Compare" - page_title "#{params[:from]}...#{params[:to]}" -- if show_new_nav? - - add_to_breadcrumbs("Repository", project_tree_path(@project)) +- add_to_breadcrumbs("Repository", project_tree_path(@project)) = render "projects/commits/head" %div{ class: container_class } diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 3467e357c49..5691024b997 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -1,7 +1,6 @@ - @no_container = true - page_title "Cycle Analytics" -- if show_new_nav? - - add_to_breadcrumbs("Project", project_path(@project)) +- add_to_breadcrumbs("Project", project_path(@project)) - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('cycle_analytics') diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index d0f723af5bf..acc80b49dd0 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -1,10 +1,8 @@ - @no_container = true - page_title "Environments" +- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) = render "projects/pipelines/head" -- if show_new_nav? - - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) - - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag("environments") diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml index 9f5a1239a82..44bd8d1f9ad 100644 --- a/app/views/projects/graphs/charts.html.haml +++ b/app/views/projects/graphs/charts.html.haml @@ -1,7 +1,6 @@ - @no_container = true - page_title "Charts" -- if show_new_nav? - - add_to_breadcrumbs("Repository", project_tree_path(@project)) +- add_to_breadcrumbs("Repository", project_tree_path(@project)) - content_for :page_specific_javascripts do = webpack_bundle_tag('common_d3') = webpack_bundle_tag('graphs') diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index f41a0d8293b..2f60078796e 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -5,8 +5,7 @@ = webpack_bundle_tag('graphs') = webpack_bundle_tag('graphs_show') -- if show_new_nav? - - add_to_breadcrumbs("Repository", project_tree_path(@project)) +- add_to_breadcrumbs("Repository", project_tree_path(@project)) = render 'projects/commits/head' diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml index 34d5a3e1831..6fb5aa45166 100644 --- a/app/views/projects/issues/_issues.html.haml +++ b/app/views/projects/issues/_issues.html.haml @@ -4,4 +4,4 @@ = render 'shared/empty_states/issues' - if @issues.present? - = paginate @issues, theme: "gitlab" + = paginate @issues, theme: "gitlab", total_pages: @total_pages diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index aacb057840d..6fcb5975707 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -13,15 +13,14 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues") -- if show_new_nav? - - content_for :breadcrumbs_extra do - = render "projects/issues/nav_btns" +- content_for :breadcrumbs_extra do + = render "projects/issues/nav_btns" - if project_issues(@project).exists? %div{ class: (container_class) } .top-area = render 'shared/issuable/nav', type: :issues - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs = render "projects/issues/nav_btns" = render 'shared/issuable/search_bar', type: :issues diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index fd7ff176c5e..04b4ed95a2d 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -37,8 +37,7 @@ %ul - if can_update_issue %li= link_to 'Edit', edit_project_issue_path(@project, @issue) - / TODO: simplify condition back #36860 - - if @issue.author && current_user != @issue.author + - 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 js-btn-issue-action #{issue_button_visibility(@issue, true)}", title: 'Close issue' diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index f5d5bc7eda9..43e23bb2200 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -46,14 +46,14 @@ %span.build-light-text Token: #{@build.trigger_request.trigger.short_token} - - if @build.trigger_request.variables + - if @build.trigger_variables.any? %p %button.btn.group.btn-group-justified.reveal-variables Reveal Variables %dl.js-build-variables.trigger-build-variables.hide - - @build.trigger_request.variables.each do |key, value| - %dt.js-build-variable.trigger-build-variable= key - %dd.js-build-value.trigger-build-value= value + - @build.trigger_variables.each do |trigger_variable| + %dt.js-build-variable.trigger-build-variable= trigger_variable[:key] + %dd.js-build-value.trigger-build-value= trigger_variable[:value] %div{ class: (@build.pipeline.stages_count > 1 ? "block" : "block-last") } %p diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml index d78891546f7..9751de0f938 100644 --- a/app/views/projects/jobs/index.html.haml +++ b/app/views/projects/jobs/index.html.haml @@ -2,8 +2,7 @@ - page_title "Jobs" = render "projects/pipelines/head" -- if show_new_nav? - - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) +- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) %div{ class: container_class } .top-area diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 4b9da02c6b8..ec9e8444ac5 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -3,7 +3,7 @@ - hide_class = '' - can_admin_label = can?(current_user, :admin_label, @project) -- if show_new_nav? && can?(current_user, :admin_label, @project) +- if can?(current_user, :admin_label, @project) - content_for :breadcrumbs_extra do = link_to "New label", new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" @@ -18,7 +18,7 @@ Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging. - if can_admin_label - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs = link_to new_project_label_path(@project), class: "btn btn-new" do New label diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml index 4e97f74dd6a..bd6f1c05949 100644 --- a/app/views/projects/merge_requests/_merge_requests.html.haml +++ b/app/views/projects/merge_requests/_merge_requests.html.haml @@ -5,4 +5,4 @@ = render 'shared/empty_states/merge_requests' - if @merge_requests.present? - = paginate @merge_requests, theme: "gitlab" + = paginate @merge_requests, theme: "gitlab", total_pages: @total_pages diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index c020e7db380..27c3002366b 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -12,9 +12,8 @@ = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'filtered_search' -- if show_new_nav? - - content_for :breadcrumbs_extra do - = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path +- content_for :breadcrumbs_extra do + = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path = render 'projects/last_push' @@ -22,7 +21,7 @@ %div{ class: container_class } .top-area = render 'shared/issuable/nav', type: :merge_requests - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path = render 'shared/issuable/search_bar', type: :merge_requests diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index e0b29b0c2e1..71ec88ef1c1 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,7 +1,7 @@ - @no_container = true - page_title 'Milestones' -- if show_new_nav? && can?(current_user, :admin_milestone, @project) +- if can?(current_user, :admin_milestone, @project) - content_for :breadcrumbs_extra do = link_to "New milestone", new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New milestone' @@ -11,10 +11,10 @@ .top-area = render 'shared/milestones_filter', counts: milestone_counts(@project.milestones) - .nav-controls{ class: ("nav-controls-new-nav" if show_new_nav?) } + .nav-controls.nav-controls-new-nav = render 'shared/milestones_sort_dropdown' - if can?(current_user, :admin_milestone, @project) - = link_to new_project_milestone_path(@project), class: "btn btn-new #{("visible-xs" if show_new_nav?)}", title: 'New milestone' do + = link_to new_project_milestone_path(@project), class: "btn btn-new visible-xs", title: 'New milestone' do New milestone .milestones diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index ab948df4a3f..2defd45a0b2 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -2,8 +2,7 @@ - page_title "Graph", @ref - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('network') -- if show_new_nav? - - add_to_breadcrumbs("Repository", project_tree_path(@project)) +- add_to_breadcrumbs("Repository", project_tree_path(@project)) = render "projects/commits/head" = render "head" %div{ class: container_class } diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml index b04f5efe1f9..fb07141d2ac 100644 --- a/app/views/projects/notes/_actions.html.haml +++ b/app/views/projects/notes/_actions.html.haml @@ -31,7 +31,7 @@ %template{ 'v-if' => 'isResolved' } = render 'shared/icons/icon_status_success_solid.svg' %template{ 'v-else' => '' } - = render 'shared/icons/icon_status_success.svg' + = render 'shared/icons/icon_resolve_discussion.svg' - if current_user - if note.emoji_awardable? diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml index 8426b29bb14..83b51bae73d 100644 --- a/app/views/projects/pipeline_schedules/index.html.haml +++ b/app/views/projects/pipeline_schedules/index.html.haml @@ -7,11 +7,11 @@ - @no_container = true - page_title _("Pipeline Schedules") -- if show_new_nav? && can?(current_user, :create_pipeline_schedule, @project) +- if can?(current_user, :create_pipeline_schedule, @project) - content_for :breadcrumbs_extra do = link_to _('New schedule'), new_namespace_project_pipeline_schedule_path(@project.namespace, @project), class: 'btn btn-create' - - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) +- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) = render "projects/pipelines/head" @@ -22,7 +22,7 @@ = render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope - if can?(current_user, :create_pipeline_schedule, @project) - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs = link_to new_project_pipeline_schedule_path(@project), class: 'btn btn-create' do %span= _('New schedule') diff --git a/app/views/projects/pipeline_schedules/new.html.haml b/app/views/projects/pipeline_schedules/new.html.haml index c7237cb96d8..cfdaf6d43bb 100644 --- a/app/views/projects/pipeline_schedules/new.html.haml +++ b/app/views/projects/pipeline_schedules/new.html.haml @@ -2,8 +2,7 @@ - @breadcrumb_link = namespace_project_pipeline_schedules_path(@project.namespace, @project) - page_title _("New Pipeline Schedule") -- if show_new_nav? - - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) +- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) %h3.page-title = _("Schedule a new pipeline") diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml index fd3ad69d85d..d710ba92cdd 100644 --- a/app/views/projects/pipelines/charts.html.haml +++ b/app/views/projects/pipelines/charts.html.haml @@ -1,7 +1,6 @@ - @no_container = true - page_title _("Charts"), _("Pipelines") -- if show_new_nav? - - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) +- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('common_d3') = page_specific_javascript_bundle_tag('graphs') diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml index 5b73e862022..8bf76b646f7 100644 --- a/app/views/projects/pipelines_settings/_show.html.haml +++ b/app/views/projects/pipelines_settings/_show.html.haml @@ -57,8 +57,21 @@ = f.check_box :public_builds %strong Public pipelines .help-block - Allow everyone to access pipelines for public and internal projects + Allow public access to pipelines and job details, including output logs and artifacts = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines'), target: '_blank' + .bs-callout.bs-callout-info + %p If enabled: + %ul + %li + For public projects, anyone can view pipelines and access job details (output logs and artifacts) + %li + For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts) + %li + For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts) + %p + If disabled, the access level will depend on the user's + permissions in the project. + %hr .form-group .checkbox diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 9f7c5a315eb..5c3339cc749 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,7 +1,6 @@ - page_title "Members" -- if show_new_nav? - - add_to_breadcrumbs("Settings", edit_project_path(@project)) +- add_to_breadcrumbs("Settings", edit_project_path(@project)) .row.prepend-top-default .col-lg-12 diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml index 8056217bb1e..3e2a24a4c32 100644 --- a/app/views/projects/services/edit.html.haml +++ b/app/views/projects/services/edit.html.haml @@ -1,8 +1,6 @@ - breadcrumb_title "Integrations" - page_title @service.title, "Services" - -- if show_new_nav? - - add_to_breadcrumbs("Settings", edit_project_path(@project)) +- add_to_breadcrumbs("Settings", edit_project_path(@project)) = render "projects/settings/head" = render 'form' diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index 78e9be18c50..15501845421 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -1,8 +1,6 @@ - @content_class = "limit-container-width" unless fluid_layout - page_title "Pipelines" - -- if show_new_nav? - - add_to_breadcrumbs("Settings", edit_project_path(@project)) +- add_to_breadcrumbs("Settings", edit_project_path(@project)) = render "projects/settings/head" diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml index 149da96d3f6..1efa5907b3a 100644 --- a/app/views/projects/settings/integrations/show.html.haml +++ b/app/views/projects/settings/integrations/show.html.haml @@ -1,7 +1,6 @@ - @content_class = "limit-container-width" unless fluid_layout - page_title 'Integrations' -- if show_new_nav? - - add_to_breadcrumbs("Settings", edit_project_path(@project)) +- add_to_breadcrumbs("Settings", edit_project_path(@project)) = render "projects/settings/head" = render 'projects/hooks/index' = render 'projects/services/index' diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml index cb37f3c7580..d4d461f6f22 100644 --- a/app/views/projects/settings/repository/show.html.haml +++ b/app/views/projects/settings/repository/show.html.haml @@ -1,8 +1,6 @@ - page_title "Repository" - @content_class = "limit-container-width" unless fluid_layout - -- if show_new_nav? - - add_to_breadcrumbs("Settings", edit_project_path(@project)) +- add_to_breadcrumbs("Settings", edit_project_path(@project)) = render "projects/settings/head" diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index ccc5fe80755..1803e7f7211 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -1,6 +1,6 @@ - page_title "Snippets" -- if show_new_nav? && can?(current_user, :create_project_snippet, @project) +- if can?(current_user, :create_project_snippet, @project) - content_for :breadcrumbs_extra do = link_to "New snippet", new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New snippet" @@ -9,7 +9,7 @@ - include_private = @project.team.member?(current_user) || current_user.admin? = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private } - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs - if can?(current_user, :create_project_snippet, @project) = link_to "New snippet", new_project_snippet_path(@project), class: "btn btn-new", title: "New snippet" diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 00000e0667c..a6fe02fcae0 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -1,11 +1,9 @@ - @no_container = true - @sort ||= sort_value_recently_updated - page_title "Tags" +- add_to_breadcrumbs("Repository", project_tree_path(@project)) = render "projects/commits/head" -- if show_new_nav? - - add_to_breadcrumbs("Repository", project_tree_path(@project)) - .flex-list{ class: container_class } .top-area.adjust .nav-text.row-main-content diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg index 10e6c49ae9f..0ef9de5fed6 100644 --- a/app/views/shared/_logo.svg +++ b/app/views/shared/_logo.svg @@ -1,4 +1,4 @@ -<svg width="28" height="28" class="tanuki-logo" viewBox="0 0 36 36"> +<svg width="24" height="24" class="tanuki-logo" viewBox="0 0 36 36"> <path class="tanuki-shape tanuki-left-ear" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/> <path class="tanuki-shape tanuki-right-ear" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/> <path class="tanuki-shape tanuki-nose" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/> diff --git a/app/views/shared/icons/_caret_down.svg b/app/views/shared/icons/_caret_down.svg new file mode 100644 index 00000000000..fd80fd0f651 --- /dev/null +++ b/app/views/shared/icons/_caret_down.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="caret-down" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></svg> diff --git a/app/views/shared/icons/_icon_arrow_right.svg.erb b/app/views/shared/icons/_icon_arrow_right.svg.erb new file mode 100644 index 00000000000..24d64eb73bd --- /dev/null +++ b/app/views/shared/icons/_icon_arrow_right.svg.erb @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></svg> diff --git a/app/views/shared/icons/_icon_resolve_discussion.svg b/app/views/shared/icons/_icon_resolve_discussion.svg new file mode 100644 index 00000000000..845562e9320 --- /dev/null +++ b/app/views/shared/icons/_icon_resolve_discussion.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill-rule="evenodd"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></svg> diff --git a/app/views/shared/icons/_icon_status_success.svg b/app/views/shared/icons/_icon_status_success.svg index 845562e9320..eed5006bebe 100755 --- a/app/views/shared/icons/_icon_status_success.svg +++ b/app/views/shared/icons/_icon_status_success.svg @@ -1 +1 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill-rule="evenodd"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></svg> diff --git a/app/views/shared/icons/_mr_bold.svg b/app/views/shared/icons/_mr_bold.svg index 5468545da2e..0f5be6e2bc8 100644 --- a/app/views/shared/icons/_mr_bold.svg +++ b/app/views/shared/icons/_mr_bold.svg @@ -1,2 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg> - +<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg> diff --git a/app/views/shared/icons/_plus_square.svg b/app/views/shared/icons/_plus_square.svg new file mode 100644 index 00000000000..7263d924f1f --- /dev/null +++ b/app/views/shared/icons/_plus_square.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M9 7V4c0-.552-.448-1-1-1s-1 .448-1 1v3H4c-.552 0-1 .448-1 1s.448 1 1 1h3v3c0 .552.448 1 1 1s1-.448 1-1V9h3c.552 0 1-.448 1-1s-.448-1-1-1H9zM3 0h10c1.657 0 3 1.343 3 3v10c0 1.657-1.343 3-3 3H3c-1.657 0-3-1.343-3-3V3c0-1.657 1.343-3 3-3z"/></svg> diff --git a/app/views/shared/icons/_thumbs_up.svg b/app/views/shared/icons/_thumbs_up.svg new file mode 100644 index 00000000000..7267462418e --- /dev/null +++ b/app/views/shared/icons/_thumbs_up.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.104 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.486.5l.138.137a1 1 0 0 1 .28.87L8.33 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></svg> diff --git a/app/views/shared/icons/_todo_done.svg b/app/views/shared/icons/_todo_done.svg new file mode 100644 index 00000000000..156dfa11df1 --- /dev/null +++ b/app/views/shared/icons/_todo_done.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0l-2.83-2.83a1 1 0 0 1 1.415-1.413l2.123 2.12zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></svg> diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml index cb706d80f23..f16bc8dd430 100644 --- a/app/views/shared/issuable/_close_reopen_button.html.haml +++ b/app/views/shared/issuable/_close_reopen_button.html.haml @@ -9,7 +9,6 @@ class: "hidden-xs hidden-sm btn btn-grouped btn-reopen js-btn-issue-action #{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 -- elsif issuable.author - / TODO: change back to else #36860 +- 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 index d8144a39b23..a38cd319e3c 100644 --- a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml +++ b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml @@ -37,15 +37,13 @@ %li.divider.droplab-item-ignore - / TODO: remove condition #36860 - - if issuable.author - %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. + %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/_form.html.haml b/app/views/shared/issuable/_form.html.haml index c016aa2abcd..bb02dfa0d3a 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -29,18 +29,6 @@ = render 'shared/issuable/form/metadata', issuable: issuable, form: form -- if issuable.can_move?(current_user) - %hr - .form-group - = label_tag :move_to_project_id, 'Move', class: 'control-label' - .col-sm-10 - .issuable-form-select-holder - = hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id), page_size: MoveToProjectFinder::PAGE_SIZE } - - %span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default', - title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' } - = icon('question-circle') - = render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form = render 'shared/issuable/form/merge_params', issuable: issuable diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index c3f25c9d255..0afa48b392c 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -3,7 +3,7 @@ = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('sidebar') -%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { "offset-top" => ("50" unless show_new_nav?), "spy" => ("affix" unless show_new_nav?), signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } +%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } .issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } } - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .block.issuable-sidebar-header @@ -34,7 +34,7 @@ Milestone = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable - = link_to 'Edit', '#', class: 'edit-link pull-right' + = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' .value.hide-collapsed - if issuable.milestone = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_remaining_days(issuable.milestone), data: { container: "body", html: 1 } @@ -60,7 +60,7 @@ Due date = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - = link_to 'Edit', '#', class: 'edit-link pull-right' + = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' .value.hide-collapsed %span.value-content - if issuable.due_date @@ -95,7 +95,7 @@ Labels = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable - = link_to 'Edit', '#', class: 'edit-link pull-right' + = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) } - if selected_labels.any? - selected_labels.each do |label| @@ -141,5 +141,22 @@ %cite{ title: project_ref } = project_ref = clipboard_button(text: project_ref, title: "Copy reference to clipboard", placement: "left") + - if current_user && issuable.can_move?(current_user) + .block.js-sidebar-move-issue-block + .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body' }, title: 'Move issue' } + = custom_icon('icon_arrow_right') + .dropdown.sidebar-move-issue-dropdown.hide-collapsed + %button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button', + data: { toggle: 'dropdown' } } + Move issue + .dropdown-menu.dropdown-menu-selectable + = dropdown_title('Move issue') + = dropdown_filter('Search project', search_id: 'sidebar-move-issue-dropdown-search') + = dropdown_content + = dropdown_loading + = dropdown_footer add_content_class: true do + %button.btn.btn-new.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ disabled: true } + Move + = icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon') %script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable, can_edit_issuable).to_json.html_safe diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml index 57392cd7fbb..58782fa5f58 100644 --- a/app/views/shared/issuable/_sidebar_assignees.html.haml +++ b/app/views/shared/issuable/_sidebar_assignees.html.haml @@ -13,7 +13,7 @@ Assignee = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable - = link_to 'Edit', '#', class: 'edit-link pull-right' + = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' - if !signed_in %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" } = sidebar_gutter_toggle_icon diff --git a/app/views/shared/issuable/form/_issue_assignee.html.haml b/app/views/shared/issuable/form/_issue_assignee.html.haml index 66091d95a91..9b2b6e572e7 100644 --- a/app/views/shared/issuable/form/_issue_assignee.html.haml +++ b/app/views/shared/issuable/form/_issue_assignee.html.haml @@ -11,7 +11,7 @@ Assignee = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable - = link_to 'Edit', '#', class: 'edit-link pull-right' + = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' .value.hide-collapsed - if assignees.any? - assignees.each do |assignee| diff --git a/app/views/shared/issuable/form/_merge_request_assignee.html.haml b/app/views/shared/issuable/form/_merge_request_assignee.html.haml index 18011d528a0..bf8613b0f0d 100644 --- a/app/views/shared/issuable/form/_merge_request_assignee.html.haml +++ b/app/views/shared/issuable/form/_merge_request_assignee.html.haml @@ -9,7 +9,7 @@ Assignee = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable - = link_to 'Edit', '#', class: 'edit-link pull-right' + = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' .value.hide-collapsed - if merge_request.assignee = link_to_member(@project, merge_request.assignee, size: 32, extra_class: 'bold') do diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index 40379f48393..f03e0ab154c 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -21,7 +21,7 @@ .title Start date - if @project && can?(current_user, :admin_milestone, @project) - = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'edit-link pull-right' + = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link pull-right' .value %span.value-content - if milestone.start_date @@ -51,7 +51,7 @@ .title.hide-collapsed Due date - if @project && can?(current_user, :admin_milestone, @project) - = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'edit-link pull-right' + = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link pull-right' .value.hide-collapsed %span.value-content - if milestone.due_date @@ -88,7 +88,7 @@ .block.merge-requests .sidebar-collapsed-icon %strong - = icon('exclamation', 'aria-hidden': 'true') + = custom_icon('mr_bold') %span= milestone.merge_requests.count .title.hide-collapsed Merge requests diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb index f34dff2d656..9b5ff17aafa 100644 --- a/app/workers/create_gpg_signature_worker.rb +++ b/app/workers/create_gpg_signature_worker.rb @@ -6,7 +6,11 @@ class CreateGpgSignatureWorker project = Project.find_by(id: project_id) return unless project + commit = project.commit(commit_sha) + + return unless commit + # This calculates and caches the signature in the database - Gitlab::Gpg::Commit.new(project, commit_sha).signature + Gitlab::Gpg::Commit.new(commit).signature end end diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb index 8b0cfcc8af8..269776a1f62 100644 --- a/app/workers/stuck_ci_jobs_worker.rb +++ b/app/workers/stuck_ci_jobs_worker.rb @@ -53,7 +53,7 @@ class StuckCiJobsWorker def drop_build(type, build, status, timeout) Rails.logger.info "#{self.class}: Dropping #{type} build #{build.id} for runner #{build.runner_id} (status: #{status}, timeout: #{timeout})" Gitlab::OptimisticLocking.retry_lock(build, 3) do |b| - b.drop + b.drop(:stuck_or_timeout_failure) end end end |