diff options
Diffstat (limited to 'app')
136 files changed, 1624 insertions, 1287 deletions
diff --git a/app/assets/images/ext_snippet_icons/ext_snippet_icons.png b/app/assets/images/ext_snippet_icons/ext_snippet_icons.png Binary files differnew file mode 100644 index 00000000000..20380adc4e5 --- /dev/null +++ b/app/assets/images/ext_snippet_icons/ext_snippet_icons.png diff --git a/app/assets/images/ext_snippet_icons/logo.png b/app/assets/images/ext_snippet_icons/logo.png Binary files differnew file mode 100644 index 00000000000..794c9cc2dbc --- /dev/null +++ b/app/assets/images/ext_snippet_icons/logo.png diff --git a/app/assets/javascripts/branches/branches_delete_modal.js b/app/assets/javascripts/branches/branches_delete_modal.js index 839e369eaf6..f34496f84c6 100644 --- a/app/assets/javascripts/branches/branches_delete_modal.js +++ b/app/assets/javascripts/branches/branches_delete_modal.js @@ -16,6 +16,7 @@ class DeleteModal { bindEvents() { this.$toggleBtns.on('click', this.setModalData.bind(this)); this.$confirmInput.on('input', this.setDeleteDisabled.bind(this)); + this.$deleteBtn.on('click', this.setDisableDeleteButton.bind(this)); } setModalData(e) { @@ -30,6 +31,16 @@ class DeleteModal { this.$deleteBtn.attr('disabled', e.currentTarget.value !== this.branchName); } + setDisableDeleteButton(e) { + if (this.$deleteBtn.is('[disabled]')) { + e.preventDefault(); + e.stopPropagation(); + return false; + } + + return true; + } + updateModal() { this.$branchName.text(this.branchName); this.$confirmInput.val(''); diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index f8dcdf3f60a..9c12b89240c 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -1,96 +1,102 @@ <script> - import _ from 'underscore'; - import { s__, sprintf } from '../../locale'; - import applicationRow from './application_row.vue'; - import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; - import { - APPLICATION_INSTALLED, - INGRESS, - } from '../constants'; +import _ from 'underscore'; +import { s__, sprintf } from '../../locale'; +import applicationRow from './application_row.vue'; +import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; +import { APPLICATION_INSTALLED, INGRESS } from '../constants'; - export default { - components: { - applicationRow, - clipboardButton, +export default { + components: { + applicationRow, + clipboardButton, + }, + props: { + applications: { + type: Object, + required: false, + default: () => ({}), }, - props: { - applications: { - type: Object, - required: false, - default: () => ({}), - }, - helpPath: { - type: String, - required: false, - default: '', - }, - ingressHelpPath: { - type: String, - required: false, - default: '', - }, - ingressDnsHelpPath: { - type: String, - required: false, - default: '', - }, - managePrometheusPath: { - type: String, - required: false, - default: '', - }, + helpPath: { + type: String, + required: false, + default: '', }, - computed: { - generalApplicationDescription() { - return sprintf( - _.escape(s__( + ingressHelpPath: { + type: String, + required: false, + default: '', + }, + ingressDnsHelpPath: { + type: String, + required: false, + default: '', + }, + managePrometheusPath: { + type: String, + required: false, + default: '', + }, + }, + computed: { + generalApplicationDescription() { + return sprintf( + _.escape( + s__( `ClusterIntegration|Install applications on your Kubernetes cluster. Read more about %{helpLink}`, - )), { - helpLink: `<a href="${this.helpPath}"> + ), + ), + { + helpLink: `<a href="${this.helpPath}"> ${_.escape(s__('ClusterIntegration|installing applications'))} </a>`, - }, - false, - ); - }, - ingressId() { - return INGRESS; - }, - ingressInstalled() { - return this.applications.ingress.status === APPLICATION_INSTALLED; - }, - ingressExternalIp() { - return this.applications.ingress.externalIp; - }, - ingressDescription() { - const extraCostParagraph = sprintf( - _.escape(s__( + }, + false, + ); + }, + ingressId() { + return INGRESS; + }, + ingressInstalled() { + return this.applications.ingress.status === APPLICATION_INSTALLED; + }, + ingressExternalIp() { + return this.applications.ingress.externalIp; + }, + ingressDescription() { + const extraCostParagraph = sprintf( + _.escape( + s__( `ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which may incur additional costs depending on - the hosting provider your Kubernetes cluster is installed on. If you are using GKE, - you can %{pricingLink}.`, - )), { - boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`, - pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer"> + the hosting provider your Kubernetes cluster is installed on. If you are using + Google Kubernetes Engine, you can %{pricingLink}.`, + ), + ), + { + boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`, + pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer"> ${_.escape(s__('ClusterIntegration|check the pricing here'))}</a>`, - }, - false, - ); + }, + false, + ); - const externalIpParagraph = sprintf( - _.escape(s__( + const externalIpParagraph = sprintf( + _.escape( + s__( `ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}`, - )), { - ingressHelpLink: `<a href="${this.ingressHelpPath}"> + ), + ), + { + ingressHelpLink: `<a href="${this.ingressHelpPath}"> ${_.escape(s__('ClusterIntegration|More information'))} </a>`, - }, - false, - ); + }, + false, + ); - return ` + return ` <p> ${extraCostParagraph} </p> @@ -98,22 +104,25 @@ ${externalIpParagraph} </p> `; - }, - prometheusDescription() { - return sprintf( - _.escape(s__( + }, + prometheusDescription() { + return sprintf( + _.escape( + s__( `ClusterIntegration|Prometheus is an open-source monitoring system with %{gitlabIntegrationLink} to monitor deployed applications.`, - )), { - gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html" + ), + ), + { + gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html" target="_blank" rel="noopener noreferrer"> ${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`, - }, - false, - ); - }, + }, + false, + ); }, - }; + }, +}; </script> <template> @@ -205,7 +214,7 @@ > {{ s__(`ClusterIntegration|The IP address is in the process of being assigned. Please check your Kubernetes - cluster or Quotas on GKE if it takes a long time.`) }} + cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) }} <a :href="ingressHelpPath" diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js index c50ac667c20..2d5bae9a9c4 100644 --- a/app/assets/javascripts/feature_highlight/feature_highlight.js +++ b/app/assets/javascripts/feature_highlight/feature_highlight.js @@ -1,19 +1,19 @@ import $ from 'jquery'; -import _ from 'underscore'; import { getSelector, - togglePopover, inserted, - mouseenter, - mouseleave, } from './feature_highlight_helper'; +import { + togglePopover, + mouseenter, + debouncedMouseleave, +} from '../shared/popover'; export function setupFeatureHighlightPopover(id, debounceTimeout = 300) { const $selector = $(getSelector(id)); const $parent = $selector.parent(); const $popoverContent = $parent.siblings('.feature-highlight-popover-content'); const hideOnScroll = togglePopover.bind($selector, false); - const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout); $selector // Setup popover @@ -29,13 +29,10 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) { `, }) .on('mouseenter', mouseenter) - .on('mouseleave', debouncedMouseleave) + .on('mouseleave', debouncedMouseleave(debounceTimeout)) .on('inserted.bs.popover', inserted) .on('show.bs.popover', () => { - window.addEventListener('scroll', hideOnScroll); - }) - .on('hide.bs.popover', () => { - window.removeEventListener('scroll', hideOnScroll); + window.addEventListener('scroll', hideOnScroll, { once: true }); }) // Display feature highlight .removeAttr('disabled'); diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js index f480e72961c..d5b97ebb264 100644 --- a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js +++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js @@ -3,20 +3,10 @@ import axios from '../lib/utils/axios_utils'; import { __ } from '../locale'; import Flash from '../flash'; import LazyLoader from '../lazy_loader'; +import { togglePopover } from '../shared/popover'; export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`; -export function togglePopover(show) { - const isAlreadyShown = this.hasClass('js-popover-show'); - if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) { - return false; - } - this.popover(show ? 'show' : 'hide'); - this.toggleClass('disable-animation js-popover-show', show); - - return true; -} - export function dismiss(highlightId) { axios.post(this.attr('data-dismiss-endpoint'), { feature_name: highlightId, @@ -27,23 +17,6 @@ export function dismiss(highlightId) { this.hide(); } -export function mouseleave() { - if (!$('.popover:hover').length > 0) { - const $featureHighlight = $(this); - togglePopover.call($featureHighlight, false); - } -} - -export function mouseenter() { - const $featureHighlight = $(this); - - const showedPopover = togglePopover.call($featureHighlight, true); - if (showedPopover) { - $('.popover') - .on('mouseleave', mouseleave.bind($featureHighlight)); - } -} - export function inserted() { const popoverId = this.getAttribute('aria-describedby'); const highlightId = this.dataset.highlight; diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index 20983666b4a..4a0a303d5a6 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -36,11 +36,11 @@ const router = new VueRouter({ base: `${gon.relative_url_root}/-/ide/`, routes: [ { - path: '/project/:namespace/:project', + path: '/project/:namespace/:project+', component: EmptyRouterComponent, children: [ { - path: ':targetmode/:branch/*', + path: ':targetmode(edit|tree|blob)/:branch/*', component: EmptyRouterComponent, }, { diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index f536ce6344b..367c45f7e2d 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -37,9 +37,9 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => { const commitMsg = sprintf( __('Your changes have been committed. Commit %{commitId} %{commitStats}'), { - commitId: `<a href="${currentProject.web_url}/commit/${ + commitId: `<a href="${currentProject.web_url}/commit/${data.short_id}" class="commit-sha">${ data.short_id - }" class="commit-sha">${data.short_id}</a>`, + }</a>`, commitStats, }, false, @@ -54,9 +54,7 @@ export const checkCommitStatus = ({ rootState }) => .then(({ data }) => { const { id } = data.commit; const selectedBranch = - rootState.projects[rootState.currentProjectId].branches[ - rootState.currentBranchId - ]; + rootState.projects[rootState.currentProjectId].branches[rootState.currentBranchId]; if (selectedBranch.workingReference !== id) { return true; @@ -135,32 +133,15 @@ export const updateFilesAfterCommit = ( if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) { router.push( - `/project/${rootState.currentProjectId}/blob/${branch}/${ - rootGetters.activeFile.path - }`, + `/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`, ); } - - dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH); }; -export const commitChanges = ({ - commit, - state, - getters, - dispatch, - rootState, -}) => { +export const commitChanges = ({ commit, state, getters, dispatch, rootState }) => { const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH; - const payload = createCommitPayload( - getters.branchName, - newBranch, - state, - rootState, - ); - const getCommitStatus = newBranch - ? Promise.resolve(false) - : dispatch('checkCommitStatus'); + const payload = createCommitPayload(getters.branchName, newBranch, state, rootState); + const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus'); commit(types.UPDATE_LOADING, true); @@ -182,28 +163,29 @@ export const commitChanges = ({ if (!data.short_id) { flash(data.message, 'alert', document, null, false, true); - return; + return null; } dispatch('setLastCommitMessage', data); dispatch('updateCommitMessage', ''); - - if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) { - dispatch( - 'redirectToUrl', - createNewMergeRequestUrl( - rootState.projects[rootState.currentProjectId].web_url, - getters.branchName, - rootState.currentBranchId, - ), - { root: true }, - ); - } else { - dispatch('updateFilesAfterCommit', { - data, - branch: getters.branchName, - }); - } + return dispatch('updateFilesAfterCommit', { + data, + branch: getters.branchName, + }) + .then(() => { + if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) { + dispatch( + 'redirectToUrl', + createNewMergeRequestUrl( + rootState.projects[rootState.currentProjectId].web_url, + getters.branchName, + rootState.currentBranchId, + ), + { root: true }, + ); + } + }) + .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH)); }) .catch(err => { let errMsg = __('Error committing changes. Please try again.'); diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js index 7f7e470c9bb..1176c040fb9 100644 --- a/app/assets/javascripts/ide/stores/mutations/tree.js +++ b/app/assets/javascripts/ide/stores/mutations/tree.js @@ -17,12 +17,8 @@ export default { }); }, [types.SET_DIRECTORY_DATA](state, { data, treePath }) { - Object.assign(state, { - trees: Object.assign(state.trees, { - [treePath]: { - tree: data, - }, - }), + Object.assign(state.trees[treePath], { + tree: data, }); }, [types.SET_LAST_COMMIT_URL](state, { tree = state, url }) { diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index e77318fef46..3f84f4b9499 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -7,11 +7,7 @@ import flash from './flash'; import BlobForkSuggestion from './blob/blob_fork_suggestion'; import initChangesDropdown from './init_changes_dropdown'; import bp from './breakpoints'; -import { - parseUrlPathname, - handleLocationHash, - isMetaClick, -} from './lib/utils/common_utils'; +import { parseUrlPathname, handleLocationHash, isMetaClick } from './lib/utils/common_utils'; import { getLocationHash } from './lib/utils/url_utility'; import initDiscussionTab from './image_diff/init_discussion_tab'; import Diff from './diff'; @@ -69,11 +65,10 @@ import Notes from './notes'; let location = window.location; export default class MergeRequestTabs { - constructor({ action, setUrl, stubLocation } = {}) { const mergeRequestTabs = document.querySelector('.js-tabs-affix'); const navbar = document.querySelector('.navbar-gitlab'); - const peek = document.getElementById('peek'); + const peek = document.getElementById('js-peek'); const paddingTop = 16; this.diffsLoaded = false; @@ -109,8 +104,7 @@ export default class MergeRequestTabs { .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) .on('click', '.js-show-tab', this.showTab); - $('.merge-request-tabs a[data-toggle="tab"]') - .on('click', this.clickTab); + $('.merge-request-tabs a[data-toggle="tab"]').on('click', this.clickTab); } // Used in tests @@ -119,8 +113,7 @@ export default class MergeRequestTabs { .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) .off('click', '.js-show-tab', this.showTab); - $('.merge-request-tabs a[data-toggle="tab"]') - .off('click', this.clickTab); + $('.merge-request-tabs a[data-toggle="tab"]').off('click', this.clickTab); } destroyPipelinesView() { @@ -183,10 +176,7 @@ export default class MergeRequestTabs { scrollToElement(container) { if (location.hash) { - const offset = 0 - ( - $('.navbar-gitlab').outerHeight() + - $('.js-tabs-affix').outerHeight() - ); + const offset = 0 - ($('.navbar-gitlab').outerHeight() + $('.js-tabs-affix').outerHeight()); const $el = $(`${container} ${location.hash}:not(.match)`); if ($el.length) { $.scrollTo($el[0], { offset }); @@ -240,9 +230,13 @@ export default class MergeRequestTabs { // Turbolinks' history. // // See https://github.com/rails/turbolinks/issues/363 - window.history.replaceState({ - url: newState, - }, document.title, newState); + window.history.replaceState( + { + url: newState, + }, + document.title, + newState, + ); return newState; } @@ -258,7 +252,8 @@ export default class MergeRequestTabs { this.toggleLoading(true); - axios.get(`${source}.json`) + axios + .get(`${source}.json`) .then(({ data }) => { document.querySelector('div#commits').innerHTML = data.html; localTimeAgo($('.js-timeago', 'div#commits')); @@ -303,7 +298,8 @@ export default class MergeRequestTabs { this.toggleLoading(true); - axios.get(`${urlPathname}.json${location.search}`) + axios + .get(`${urlPathname}.json${location.search}`) .then(({ data }) => { const $container = $('#diffs'); $container.html(data.html); @@ -332,8 +328,7 @@ export default class MergeRequestTabs { cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'), suggestionSections: $(el).find('.js-file-fork-suggestion-section'), actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'), - }) - .init(); + }).init(); }); // Scroll any linked note into view @@ -388,8 +383,7 @@ export default class MergeRequestTabs { resetViewContainer() { if (this.fixedLayoutPref !== null) { - $('.content-wrapper .container-fluid') - .toggleClass('container-limited', this.fixedLayoutPref); + $('.content-wrapper .container-fluid').toggleClass('container-limited', this.fixedLayoutPref); } } @@ -438,12 +432,11 @@ export default class MergeRequestTabs { const $diffTabs = $('#diff-notes-app'); - $tabs.off('affix.bs.affix affix-top.bs.affix') + $tabs + .off('affix.bs.affix affix-top.bs.affix') .affix({ offset: { - top: () => ( - $diffTabs.offset().top - $tabs.height() - $fixedNav.height() - ), + top: () => $diffTabs.offset().top - $tabs.height() - $fixedNav.height(), }, }) .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() })) diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index e6e3a66aa20..325fa570f37 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import flash from './flash'; +import { mouseenter, debouncedMouseleave, togglePopover } from './shared/popover'; export default class Milestone { constructor() { @@ -43,4 +44,25 @@ export default class Milestone { .catch(() => flash('Error loading milestone tab')); } } + + static initDeprecationMessage() { + const deprecationMesssageContainer = document.querySelector('.js-milestone-deprecation-message'); + + if (!deprecationMesssageContainer) return; + + const deprecationMessage = deprecationMesssageContainer.querySelector('.js-milestone-deprecation-message-template').innerHTML; + const $popover = $('.js-popover-link', deprecationMesssageContainer); + const hideOnScroll = togglePopover.bind($popover, false); + + $popover.popover({ + content: deprecationMessage, + html: true, + placement: 'bottom', + }) + .on('mouseenter', mouseenter) + .on('mouseleave', debouncedMouseleave()) + .on('show.bs.popover', () => { + window.addEventListener('scroll', hideOnScroll, { once: true }); + }); + } } diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index a07af3ee93a..2121907dff0 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1192,12 +1192,12 @@ export default class Notes { addForm = false; let lineTypeSelector = ''; rowCssToAdd = - '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content discussion-notes"></div></td></tr>'; + '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>'; // In parallel view, look inside the correct left/right pane if (this.isParallelView()) { lineTypeSelector = `.${lineType}`; rowCssToAdd = - '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content discussion-notes"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content discussion-notes"></div></td></tr>'; + '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>'; } const notesContentSelector = `.notes_content${lineTypeSelector} .content`; let notesContent = targetRow.find(notesContentSelector); diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 648fa6ff804..396a675b4ac 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -317,10 +317,10 @@ Please check your network connection and try again.`; <note-signed-out-widget v-if="!isLoggedIn" /> <discussion-locked-widget issuable-type="issue" - v-else-if="!canCreateNote" + v-else-if="isLocked(getNoteableData) && !canCreateNote" /> <ul - v-else + v-else-if="canCreateNote" class="notes notes-form timeline"> <li class="timeline-entry"> <div class="timeline-entry-inner"> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index a7e2d857013..626b0799581 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -40,6 +40,10 @@ export default { type: Boolean, required: true, }, + canAwardEmoji: { + type: Boolean, + required: true, + }, canDelete: { type: Boolean, required: true, @@ -74,9 +78,6 @@ export default { shouldShowActionsDropdown() { return this.currentUserId && (this.canEdit || this.canReportAsAbuse); }, - canAddAwardEmoji() { - return this.currentUserId; - }, isAuthoredByCurrentUser() { return this.authorId === this.currentUserId; }, @@ -149,7 +150,7 @@ export default { </button> </div> <div - v-if="canAddAwardEmoji" + v-if="canAwardEmoji" class="note-actions-item"> <a v-tooltip diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index 6cb8229e268..e8fd155a1ee 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -28,6 +28,10 @@ export default { type: Number, required: true, }, + canAwardEmoji: { + type: Boolean, + required: true, + }, }, computed: { ...mapGetters(['getUserData']), @@ -67,9 +71,6 @@ export default { isAuthoredByMe() { return this.noteAuthorId === this.getUserData.id; }, - isLoggedIn() { - return this.getUserData.id; - }, }, created() { this.emojiSmiling = emojiSmiling; @@ -156,7 +157,7 @@ export default { return title; }, handleAward(awardName) { - if (!this.isLoggedIn) { + if (!this.canAwardEmoji) { return; } @@ -208,7 +209,7 @@ export default { </span> </button> <div - v-if="isLoggedIn" + v-if="canAwardEmoji" class="award-menu-holder"> <button v-tooltip diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index 069f94c5845..0cb626c14f4 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -112,6 +112,7 @@ export default { :note-author-id="note.author.id" :awards="note.award_emoji" :toggle-award-path="note.toggle_award_path" + :can-award-emoji="note.current_user.can_award_emoji" /> <note-attachment v-if="note.attachment" diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 476b15aca4a..e0f883a8e08 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -258,7 +258,9 @@ Please check your network connection and try again.`; :key="note.id" /> </ul> - <div class="discussion-reply-holder"> + <div + :class="{ 'is-replying': isReplying }" + class="discussion-reply-holder"> <template v-if="!isReplying && canReply"> <div class="btn-group-justified discussion-with-resolve-btn" diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 3554027d2b4..566f5c68e66 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -177,6 +177,7 @@ export default { :note-id="note.id" :access-level="note.human_access" :can-edit="note.current_user.can_edit" + :can-award-emoji="note.current_user.can_award_emoji" :can-delete="note.current_user.can_edit" :can-report-as-abuse="canReportAsAbuse" :report-abuse-path="note.report_abuse_path" diff --git a/app/assets/javascripts/pages/dashboard/milestones/show/index.js b/app/assets/javascripts/pages/dashboard/milestones/show/index.js index 397149aaa9e..8b529585898 100644 --- a/app/assets/javascripts/pages/dashboard/milestones/show/index.js +++ b/app/assets/javascripts/pages/dashboard/milestones/show/index.js @@ -6,4 +6,6 @@ document.addEventListener('DOMContentLoaded', () => { new Milestone(); // eslint-disable-line no-new new Sidebar(); // eslint-disable-line no-new new MountMilestoneSidebar(); // eslint-disable-line no-new + + Milestone.initDeprecationMessage(); }); diff --git a/app/assets/javascripts/pages/groups/milestones/show/index.js b/app/assets/javascripts/pages/groups/milestones/show/index.js index 88f40b5278e..74cc4ba42c1 100644 --- a/app/assets/javascripts/pages/groups/milestones/show/index.js +++ b/app/assets/javascripts/pages/groups/milestones/show/index.js @@ -1,3 +1,8 @@ import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; +import Milestone from '~/milestone'; -document.addEventListener('DOMContentLoaded', initMilestonesShow); +document.addEventListener('DOMContentLoaded', () => { + initMilestonesShow(); + + Milestone.initDeprecationMessage(); +}); diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js index be37df36be8..628913483c6 100644 --- a/app/assets/javascripts/pages/projects/edit/index.js +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -1,12 +1,12 @@ import initSettingsPanels from '~/settings_panels'; import setupProjectEdit from '~/project_edit'; import initConfirmDangerModal from '~/confirm_danger_modal'; -import ProjectNew from '../shared/project_new'; +import initProjectLoadingSpinner from '../shared/save_project_loader'; import projectAvatar from '../shared/project_avatar'; import initProjectPermissionsSettings from '../shared/permissions'; document.addEventListener('DOMContentLoaded', () => { - new ProjectNew(); // eslint-disable-line no-new + initProjectLoadingSpinner(); setupProjectEdit(); // Initialize expandable settings panels initSettingsPanels(); diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js index ea6fd961393..7db644e2477 100644 --- a/app/assets/javascripts/pages/projects/new/index.js +++ b/app/assets/javascripts/pages/projects/new/index.js @@ -1,9 +1,9 @@ -import ProjectNew from '../shared/project_new'; +import initProjectLoadingSpinner from '../shared/save_project_loader'; import initProjectVisibilitySelector from '../../../project_visibility'; import initProjectNew from '../../../projects/project_new'; document.addEventListener('DOMContentLoaded', () => { - new ProjectNew(); // eslint-disable-line no-new + initProjectLoadingSpinner(); initProjectVisibilitySelector(); initProjectNew.bindEvents(); }); diff --git a/app/assets/javascripts/pages/projects/shared/project_new.js b/app/assets/javascripts/pages/projects/shared/project_new.js deleted file mode 100644 index 56d5574aa2f..00000000000 --- a/app/assets/javascripts/pages/projects/shared/project_new.js +++ /dev/null @@ -1,152 +0,0 @@ -/* eslint-disable func-names, no-var, no-underscore-dangle, prefer-template, prefer-arrow-callback*/ - -import $ from 'jquery'; -import VisibilitySelect from '../../../visibility_select'; - -function highlightChanges($elm) { - $elm.addClass('highlight-changes'); - setTimeout(() => $elm.removeClass('highlight-changes'), 10); -} - -export default class ProjectNew { - constructor() { - this.toggleSettings = this.toggleSettings.bind(this); - this.$selects = $('.features select'); - this.$repoSelects = this.$selects.filter('.js-repo-select'); - this.$projectSelects = this.$selects.not('.js-repo-select'); - - $('.project-edit-container').on('ajax:before', () => { - $('.project-edit-container').hide(); - return $('.save-project-loader').show(); - }); - - this.initVisibilitySelect(); - - this.toggleSettings(); - this.toggleSettingsOnclick(); - this.toggleRepoVisibility(); - } - - initVisibilitySelect() { - const visibilityContainer = document.querySelector('.js-visibility-select'); - if (!visibilityContainer) return; - const visibilitySelect = new VisibilitySelect(visibilityContainer); - visibilitySelect.init(); - - const $visibilitySelect = $(visibilityContainer).find('select'); - let projectVisibility = $visibilitySelect.val(); - const PROJECT_VISIBILITY_PRIVATE = '0'; - - $visibilitySelect.on('change', () => { - const newProjectVisibility = $visibilitySelect.val(); - - if (projectVisibility !== newProjectVisibility) { - this.$projectSelects.each((idx, select) => { - const $select = $(select); - const $options = $select.find('option'); - const values = $.map($options, e => e.value); - - // if switched to "private", limit visibility options - if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) { - if ($select.val() !== values[0] && $select.val() !== values[1]) { - $select.val(values[1]).trigger('change'); - highlightChanges($select); - } - $options.slice(2).disable(); - } - - // if switched from "private", increase visibility for non-disabled options - if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) { - $options.enable(); - if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) { - $select.val(values[values.length - 1]).trigger('change'); - highlightChanges($select); - } - } - }); - - projectVisibility = newProjectVisibility; - } - }); - } - - toggleSettings() { - this.$selects.each(function () { - var $select = $(this); - var className = $select.data('field') - .replace(/_/g, '-') - .replace('access-level', 'feature'); - ProjectNew._showOrHide($select, '.' + className); - }); - } - - toggleSettingsOnclick() { - this.$selects.on('change', this.toggleSettings); - } - - static _showOrHide(checkElement, container) { - const $container = $(container); - - if ($(checkElement).val() !== '0') { - return $container.show(); - } - return $container.hide(); - } - - toggleRepoVisibility() { - var $repoAccessLevel = $('.js-repo-access-level select'); - var $lfsEnabledOption = $('.js-lfs-enabled select'); - var containerRegistry = document.querySelectorAll('.js-container-registry')[0]; - var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled'); - var prevSelectedVal = parseInt($repoAccessLevel.val(), 10); - - this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']") - .nextAll() - .hide(); - - $repoAccessLevel - .off('change') - .on('change', function () { - var selectedVal = parseInt($repoAccessLevel.val(), 10); - - this.$repoSelects.each(function () { - var $this = $(this); - var repoSelectVal = parseInt($this.val(), 10); - - $this.find('option').enable(); - - if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) { - $this.val(selectedVal).trigger('change'); - highlightChanges($this); - } - - $this.find("option[value='" + selectedVal + "']").nextAll().disable(); - }); - - if (selectedVal) { - this.$repoSelects.removeClass('disabled'); - - if ($lfsEnabledOption.length) { - $lfsEnabledOption.removeClass('disabled'); - highlightChanges($lfsEnabledOption); - } - if (containerRegistry) { - containerRegistry.style.display = ''; - } - } else { - this.$repoSelects.addClass('disabled'); - - if ($lfsEnabledOption.length) { - $lfsEnabledOption.val('false').addClass('disabled'); - highlightChanges($lfsEnabledOption); - } - if (containerRegistry) { - containerRegistry.style.display = 'none'; - containerRegistryCheckbox.checked = false; - } - } - - prevSelectedVal = selectedVal; - }.bind(this)); - } -} diff --git a/app/assets/javascripts/pages/projects/shared/save_project_loader.js b/app/assets/javascripts/pages/projects/shared/save_project_loader.js new file mode 100644 index 00000000000..aa3589ac88d --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/save_project_loader.js @@ -0,0 +1,12 @@ +import $ from 'jquery'; + +export default function initProjectLoadingSpinner() { + const $formContainer = $('.project-edit-container'); + const $loadingSpinner = $('.save-project-loader'); + + // show loading spinner when saving + $formContainer.on('ajax:before', () => { + $formContainer.hide(); + $loadingSpinner.show(); + }); +} diff --git a/app/assets/javascripts/pages/projects/snippets/show/index.js b/app/assets/javascripts/pages/projects/snippets/show/index.js index a134599cb04..c35b9c30058 100644 --- a/app/assets/javascripts/pages/projects/snippets/show/index.js +++ b/app/assets/javascripts/pages/projects/snippets/show/index.js @@ -1,11 +1,13 @@ import initNotes from '~/init_notes'; import ZenMode from '~/zen_mode'; -import LineHighlighter from '../../../../line_highlighter'; -import BlobViewer from '../../../../blob/viewer'; +import LineHighlighter from '~/line_highlighter'; +import BlobViewer from '~/blob/viewer'; +import snippetEmbed from '~/snippet/snippet_embed'; document.addEventListener('DOMContentLoaded', () => { new LineHighlighter(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new initNotes(); new ZenMode(); // eslint-disable-line no-new + snippetEmbed(); }); diff --git a/app/assets/javascripts/pages/snippets/show/index.js b/app/assets/javascripts/pages/snippets/show/index.js index f548b9fad65..26936110402 100644 --- a/app/assets/javascripts/pages/snippets/show/index.js +++ b/app/assets/javascripts/pages/snippets/show/index.js @@ -1,11 +1,13 @@ -import LineHighlighter from '../../../line_highlighter'; -import BlobViewer from '../../../blob/viewer'; -import ZenMode from '../../../zen_mode'; -import initNotes from '../../../init_notes'; +import LineHighlighter from '~/line_highlighter'; +import BlobViewer from '~/blob/viewer'; +import ZenMode from '~/zen_mode'; +import initNotes from '~/init_notes'; +import snippetEmbed from '~/snippet/snippet_embed'; document.addEventListener('DOMContentLoaded', () => { new LineHighlighter(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new initNotes(); new ZenMode(); // eslint-disable-line no-new + snippetEmbed(); }); diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js index 3ebfaa87a4e..bc71911ae35 100644 --- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js +++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js @@ -10,29 +10,25 @@ export default class PerformanceBarService { } static registerInterceptor(peekUrl, callback) { - vueResourceInterceptor = (request, next) => { - next(response => { - const requestId = response.headers['x-request-id']; - const requestUrl = response.url; - - if (requestUrl !== peekUrl && requestId) { - callback(requestId, requestUrl); - } - }); - }; - - Vue.http.interceptors.push(vueResourceInterceptor); - - return axios.interceptors.response.use(response => { + const interceptor = response => { const requestId = response.headers['x-request-id']; - const requestUrl = response.config.url; + // Get the request URL from response.config for Axios, and response for + // Vue Resource. + const requestUrl = (response.config || response).url; + const cachedResponse = response.headers['x-gitlab-from-cache'] === 'true'; - if (requestUrl !== peekUrl && requestId) { + if (requestUrl !== peekUrl && requestId && !cachedResponse) { callback(requestId, requestUrl); } return response; - }); + }; + + vueResourceInterceptor = (request, next) => next(interceptor); + + Vue.http.interceptors.push(vueResourceInterceptor); + + return axios.interceptors.response.use(interceptor); } static removeInterceptor(interceptor) { diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index b3fcaf0ccd1..32cf3dba3c3 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -1,5 +1,4 @@ <script> - import $ from 'jquery'; /** * Renders each stage of the pipeline mini graph. @@ -13,8 +12,11 @@ * 3. Merge request widget * 4. Commit widget */ - import axios from '../../lib/utils/axios_utils'; + + import $ from 'jquery'; import Flash from '../../flash'; + import axios from '../../lib/utils/axios_utils'; + import eventHub from '../event_hub'; import Icon from '../../vue_shared/components/icon.vue'; import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; @@ -82,6 +84,7 @@ methods: { onClickStage() { if (!this.isDropdownOpen()) { + eventHub.$emit('clickedDropdown'); this.isLoading = true; this.fetchJobs(); } diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js new file mode 100644 index 00000000000..b384c7500e7 --- /dev/null +++ b/app/assets/javascripts/pipelines/constants.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export const CANCEL_REQUEST = 'CANCEL_REQUEST'; diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index 522a4277bd7..6d87f75ae8e 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -7,6 +7,7 @@ import SvgBlankState from '../components/blank_state.vue'; import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; import PipelinesTableComponent from '../components/pipelines_table.vue'; import eventHub from '../event_hub'; +import { CANCEL_REQUEST } from '../constants'; export default { components: { @@ -52,34 +53,58 @@ export default { }); eventHub.$on('postAction', this.postAction); + eventHub.$on('clickedDropdown', this.updateTable); }, beforeDestroy() { eventHub.$off('postAction', this.postAction); + eventHub.$off('clickedDropdown', this.updateTable); }, destroyed() { this.poll.stop(); }, methods: { + updateTable() { + // Cancel ongoing request + if (this.isMakingRequest) { + this.service.cancelationSource.cancel(CANCEL_REQUEST); + } + // Stop polling + this.poll.stop(); + // Update the table + return this.getPipelines() + .then(() => this.poll.restart()); + }, fetchPipelines() { if (!this.isMakingRequest) { this.isLoading = true; - this.service.getPipelines(this.requestData) - .then(response => this.successCallback(response)) - .catch(() => this.errorCallback()); + this.getPipelines(); } }, + getPipelines() { + return this.service.getPipelines(this.requestData) + .then(response => this.successCallback(response)) + .catch((error) => this.errorCallback(error)); + }, setCommonData(pipelines) { this.store.storePipelines(pipelines); this.isLoading = false; this.updateGraphDropdown = true; this.hasMadeRequest = true; + + // In case the previous polling request returned an error, we need to reset it + if (this.hasError) { + this.hasError = false; + } }, - errorCallback() { - this.hasError = true; - this.isLoading = false; - this.updateGraphDropdown = false; + errorCallback(error) { this.hasMadeRequest = true; + this.isLoading = false; + + if (error && error.message && error.message !== CANCEL_REQUEST) { + this.hasError = true; + this.updateGraphDropdown = false; + } }, setIsMakingRequest(isMakingRequest) { this.isMakingRequest = isMakingRequest; diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js index 001286f5d52..59c8b9c58e5 100644 --- a/app/assets/javascripts/pipelines/services/pipelines_service.js +++ b/app/assets/javascripts/pipelines/services/pipelines_service.js @@ -19,8 +19,13 @@ export default class PipelinesService { getPipelines(data = {}) { const { scope, page } = data; + const CancelToken = axios.CancelToken; + + this.cancelationSource = CancelToken.source(); + return axios.get(this.endpoint, { params: { scope, page }, + cancelToken: this.cancelationSource.token, }); } diff --git a/app/assets/javascripts/shared/popover.js b/app/assets/javascripts/shared/popover.js new file mode 100644 index 00000000000..3fc03553bdd --- /dev/null +++ b/app/assets/javascripts/shared/popover.js @@ -0,0 +1,33 @@ +import $ from 'jquery'; +import _ from 'underscore'; + +export function togglePopover(show) { + const isAlreadyShown = this.hasClass('js-popover-show'); + if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) { + return false; + } + this.popover(show ? 'show' : 'hide'); + this.toggleClass('disable-animation js-popover-show', show); + + return true; +} + +export function mouseleave() { + if (!$('.popover:hover').length > 0) { + const $popover = $(this); + togglePopover.call($popover, false); + } +} + +export function mouseenter() { + const $popover = $(this); + + const showedPopover = togglePopover.call($popover, true); + if (showedPopover) { + $('.popover').on('mouseleave', mouseleave.bind($popover)); + } +} + +export function debouncedMouseleave(debounceTimeout = 300) { + return _.debounce(mouseleave, debounceTimeout); +} diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js index 25f39e4fdb6..9f69f110d06 100644 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js @@ -1,12 +1,15 @@ +import { visitUrl } from './lib/utils/url_utility'; + /** * Helper function that finds the href of the fiven selector and updates the location. * * @param {String} selector */ -export default (selector) => { - const link = document.querySelector(selector).getAttribute('href'); +export default function findAndFollowLink(selector) { + const element = document.querySelector(selector); + const link = element && element.getAttribute('href'); if (link) { - window.location = link; + visitUrl(link); } -}; +} diff --git a/app/assets/javascripts/snippet/snippet_embed.js b/app/assets/javascripts/snippet/snippet_embed.js new file mode 100644 index 00000000000..81ec483f2d9 --- /dev/null +++ b/app/assets/javascripts/snippet/snippet_embed.js @@ -0,0 +1,23 @@ +export default () => { + const { protocol, host, pathname } = location; + const shareBtn = document.querySelector('.js-share-btn'); + const embedBtn = document.querySelector('.js-embed-btn'); + const snippetUrlArea = document.querySelector('.js-snippet-url-area'); + const embedAction = document.querySelector('.js-embed-action'); + const url = `${protocol}//${host + pathname}`; + + shareBtn.addEventListener('click', () => { + shareBtn.classList.add('is-active'); + embedBtn.classList.remove('is-active'); + snippetUrlArea.value = url; + embedAction.innerText = 'Share'; + }); + + embedBtn.addEventListener('click', () => { + embedBtn.classList.add('is-active'); + shareBtn.classList.remove('is-active'); + const scriptTag = `<script src="${url}.js"></script>`; + snippetUrlArea.value = scriptTag; + embedAction.innerText = 'Embed'; + }); +}; diff --git a/app/assets/javascripts/visibility_select.js b/app/assets/javascripts/visibility_select.js deleted file mode 100644 index 0c928d0d5f6..00000000000 --- a/app/assets/javascripts/visibility_select.js +++ /dev/null @@ -1,21 +0,0 @@ -export default class VisibilitySelect { - constructor(container) { - if (!container) throw new Error('VisibilitySelect requires a container element as argument 1'); - this.container = container; - this.helpBlock = this.container.querySelector('.help-block'); - this.select = this.container.querySelector('select'); - } - - init() { - if (this.select) { - this.updateHelpText(); - this.select.addEventListener('change', this.updateHelpText.bind(this)); - } else { - this.helpBlock.textContent = this.container.querySelector('.js-locked').dataset.helpBlock; - } - } - - updateHelpText() { - this.helpBlock.textContent = this.select.querySelector('option:checked').dataset.description; - } -} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue index 95c8b0a4c55..f012f9c6772 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue @@ -146,8 +146,8 @@ export default { </p> <p v-if="shouldShowMemoryGraph" - class="usage-info js-usage-info"> - {{ memoryChangeMessage }} + class="usage-info js-usage-info" + v-html="memoryChangeMessage"> </p> <p v-if="shouldShowLoadFailure" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js deleted file mode 100644 index 4d9a2ca530f..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js +++ /dev/null @@ -1,18 +0,0 @@ -import statusIcon from '../mr_widget_status_icon.vue'; - -export default { - name: 'MRWidgetPipelineBlocked', - components: { - statusIcon, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon status="warning" :show-disabled-button="true" /> - <div class="media-body space-children"> - <span class="bold"> - The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure - </span> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue new file mode 100644 index 00000000000..8d55477929f --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue @@ -0,0 +1,25 @@ +<script> +import statusIcon from '../mr_widget_status_icon.vue'; + +export default { + name: 'PipelineFailed', + components: { + statusIcon, + }, +}; +</script> + +<template> + <div class="mr-widget-body media"> + <status-icon + status="warning" + :show-disabled-button="true" + /> + <div class="media-body space-children"> + <span class="bold"> + {{ s__(`mrWidget|The pipeline for this merge request failed. +Please retry the job or push a new commit to fix the failure`) }} + </span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 3c781ccddc8..0264625a526 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -1,3 +1,4 @@ +<script> import successSvg from 'icons/_icon_status_success.svg'; import warningSvg from 'icons/_icon_status_warning.svg'; import simplePoll from '~/lib/utils/simple_poll'; @@ -7,7 +8,10 @@ import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; export default { - name: 'MRWidgetReadyToMerge', + name: 'ReadyToMerge', + components: { + statusIcon, + }, props: { mr: { type: Object, required: true }, service: { type: Object, required: true }, @@ -26,9 +30,6 @@ export default { warningSvg, }; }, - components: { - statusIcon, - }, computed: { shouldShowMergeWhenPipelineSucceedsText() { return this.mr.isPipelineActive; @@ -217,136 +218,146 @@ export default { }); }, }, - template: ` - <div class="mr-widget-body media"> - <status-icon :status="iconClass" /> - <div class="media-body"> - <div class="mr-widget-body-controls media space-children"> - <span class="btn-group append-bottom-5"> - <button - @click="handleMergeButtonClick()" - :disabled="isMergeButtonDisabled" - :class="mergeButtonClass" - type="button" - class="qa-merge-button"> - <i - v-if="isMakingRequest" - class="fa fa-spinner fa-spin" - aria-hidden="true" /> - {{mergeButtonText}} - </button> +}; +</script> + +<template> + <div class="mr-widget-body media"> + <status-icon :status="iconClass" /> + <div class="media-body"> + <div class="mr-widget-body-controls media space-children"> + <span class="btn-group append-bottom-5"> + <button + @click="handleMergeButtonClick()" + :disabled="isMergeButtonDisabled" + :class="mergeButtonClass" + type="button" + class="qa-merge-button"> + <i + v-if="isMakingRequest" + class="fa fa-spinner fa-spin" + aria-hidden="true" + ></i> + {{ mergeButtonText }} + </button> + <button + v-if="shouldShowMergeOptionsDropdown" + :disabled="isMergeButtonDisabled" + type="button" + class="btn btn-sm btn-info dropdown-toggle js-merge-moment" + data-toggle="dropdown" + aria-label="Select merge moment"> + <i + class="fa fa-chevron-down" + aria-hidden="true" + ></i> + </button> + <ul + v-if="shouldShowMergeOptionsDropdown" + class="dropdown-menu dropdown-menu-right" + role="menu"> + <li> + <a + @click.prevent="handleMergeButtonClick(true)" + class="merge_when_pipeline_succeeds" + href="#"> + <span class="media"> + <span + v-html="successSvg" + class="merge-opt-icon" + aria-hidden="true"></span> + <span class="media-body merge-opt-title">Merge when pipeline succeeds</span> + </span> + </a> + </li> + <li> + <a + @click.prevent="handleMergeButtonClick(false, true)" + class="accept-merge-request" + href="#"> + <span class="media"> + <span + v-html="warningSvg" + class="merge-opt-icon" + aria-hidden="true"></span> + <span class="media-body merge-opt-title">Merge immediately</span> + </span> + </a> + </li> + </ul> + </span> + <div class="media-body-wrap space-children"> + <template v-if="shouldShowMergeControls()"> + <label v-if="mr.canRemoveSourceBranch"> + <input + id="remove-source-branch-input" + v-model="removeSourceBranch" + class="js-remove-source-branch-checkbox" + :disabled="isRemoveSourceBranchButtonDisabled" + type="checkbox"/> Remove source branch + </label> + + <!-- Placeholder for EE extension of this component --> + <squash-before-merge + v-if="shouldShowSquashBeforeMerge" + :mr="mr" + :is-merge-button-disabled="isMergeButtonDisabled" /> + + <span + v-if="mr.ffOnlyEnabled" + class="js-fast-forward-message"> + Fast-forward merge without a merge commit + </span> <button - v-if="shouldShowMergeOptionsDropdown" + v-else + @click="toggleCommitMessageEditor" :disabled="isMergeButtonDisabled" - type="button" - class="btn btn-sm btn-info dropdown-toggle js-merge-moment" - data-toggle="dropdown" - aria-label="Select merge moment"> - <i - class="fa fa-chevron-down" - aria-hidden="true" /> + class="js-modify-commit-message-button btn btn-default btn-xs" + type="button"> + Modify commit message </button> - <ul - v-if="shouldShowMergeOptionsDropdown" - class="dropdown-menu dropdown-menu-right" - role="menu"> - <li> - <a - @click.prevent="handleMergeButtonClick(true)" - class="merge_when_pipeline_succeeds" - href="#"> - <span class="media"> - <span - v-html="successSvg" - class="merge-opt-icon" - aria-hidden="true"></span> - <span class="media-body merge-opt-title">Merge when pipeline succeeds</span> - </span> - </a> - </li> - <li> - <a - @click.prevent="handleMergeButtonClick(false, true)" - class="accept-merge-request" - href="#"> - <span class="media"> - <span - v-html="warningSvg" - class="merge-opt-icon" - aria-hidden="true"></span> - <span class="media-body merge-opt-title">Merge immediately</span> - </span> - </a> - </li> - </ul> - </span> - <div class="media-body-wrap space-children"> - <template v-if="shouldShowMergeControls()"> - <label v-if="mr.canRemoveSourceBranch"> - <input - id="remove-source-branch-input" - v-model="removeSourceBranch" - class="js-remove-source-branch-checkbox" - :disabled="isRemoveSourceBranchButtonDisabled" - type="checkbox"/> Remove source branch - </label> - - <!-- Placeholder for EE extension of this component --> - <squash-before-merge - v-if="shouldShowSquashBeforeMerge" - :mr="mr" - :is-merge-button-disabled="isMergeButtonDisabled" /> - - <span - v-if="mr.ffOnlyEnabled" - class="js-fast-forward-message"> - Fast-forward merge without a merge commit - </span> - <button - v-else - @click="toggleCommitMessageEditor" - :disabled="isMergeButtonDisabled" - class="js-modify-commit-message-button btn btn-default btn-xs" - type="button"> - Modify commit message - </button> - </template> - <template v-else> - <span class="bold js-resolve-mr-widget-items-message"> - You can only merge once the items above are resolved - </span> - </template> - </div> + </template> + <template v-else> + <span class="bold js-resolve-mr-widget-items-message"> + You can only merge once the items above are resolved + </span> + </template> </div> - <div - v-if="showCommitMessageEditor" - class="prepend-top-default commit-message-editor"> - <div class="form-group clearfix"> - <label - class="control-label" - for="commit-message"> - Commit message - </label> - <div class="col-sm-10"> - <div class="commit-message-container"> - <div class="max-width-marker"></div> - <textarea - v-model="commitMessage" - class="form-control js-commit-message" - required="required" - rows="14" - name="Commit message"></textarea> - </div> - <p class="hint">Try to keep the first line under 52 characters and the others under 72</p> - <div class="hint"> - <a - @click.prevent="updateCommitMessage" - href="#">{{commitMessageLinkTitle}}</a> - </div> + </div> + <div + v-if="showCommitMessageEditor" + class="prepend-top-default commit-message-editor"> + <div class="form-group clearfix"> + <label + class="control-label" + for="commit-message"> + Commit message + </label> + <div class="col-sm-10"> + <div class="commit-message-container"> + <div class="max-width-marker"></div> + <textarea + id="commit-message" + v-model="commitMessage" + class="form-control js-commit-message" + required="required" + rows="14" + name="Commit message"></textarea> + </div> + <p class="hint"> + Try to keep the first line under 52 characters and the others under 72 + </p> + <div class="hint"> + <a + @click.prevent="updateCommitMessage" + href="#" + > + {{ commitMessageLinkTitle }} + </a> </div> </div> </div> </div> </div> - `, -}; + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js index ed15fc6ab0f..3b5c973e4a0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js +++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js @@ -27,11 +27,11 @@ export { default as ConflictsState } from './components/states/mr_widget_conflic export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue'; export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue'; export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue'; -export { default as ReadyToMergeState } from './components/states/mr_widget_ready_to_merge'; +export { default as ReadyToMergeState } from './components/states/ready_to_merge.vue'; export { default as ShaMismatchState } from './components/states/sha_mismatch.vue'; export { default as UnresolvedDiscussionsState } from './components/states/unresolved_discussions.vue'; export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue'; -export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed'; +export { default as PipelineFailedState } from './components/states/pipeline_failed.vue'; export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue'; export { default as RebaseState } from './components/states/mr_widget_rebase.vue'; export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue'; diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index 97789636787..b8875d04488 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -175,7 +175,7 @@ </a> </span> <span v-else> - Cant find HEAD commit for this branch + Can't find HEAD commit for this branch </span> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue index b06493e6c66..16304e4815d 100644 --- a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue +++ b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue @@ -9,7 +9,7 @@ lines: { type: Number, required: false, - default: 6, + default: 3, }, }, computed: { diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 0665622fe4a..f2950308019 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -37,7 +37,11 @@ /* * Code highlight */ -@import "highlight/**/*"; +@import "highlight/dark"; +@import "highlight/monokai"; +@import "highlight/solarized_dark"; +@import "highlight/solarized_light"; +@import "highlight/white"; /* * Styles for JS behaviors. diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 728f9a27aca..14cd32da9eb 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -187,12 +187,9 @@ a { animation: fadeInFull $fade-in-duration 1; } - .animation-container { - background: $repo-editor-grey; height: 40px; overflow: hidden; - position: relative; &.animation-container-small { height: 12px; @@ -205,60 +202,43 @@ a { } } - &::before { - animation-duration: 1s; - animation-fill-mode: forwards; - animation-iteration-count: infinite; - animation-name: blockTextShine; - animation-timing-function: linear; - background-image: $repo-editor-linear-gradient; - background-repeat: no-repeat; - background-size: 800px 45px; - content: ' '; - display: block; - height: 100%; + [class^="skeleton-line-"] { position: relative; - } - - div { - background: $white-light; - height: 6px; - left: 0; - position: absolute; - right: 0; - } - - .skeleton-line-1 { - left: 0; - top: 8px; - } - - .skeleton-line-2 { - left: 150px; - top: 0; + background-color: $theme-gray-100; height: 10px; - } + overflow: hidden; - .skeleton-line-3 { - left: 0; - top: 23px; - } + &:not(:last-of-type) { + margin-bottom: 4px; + } - .skeleton-line-4 { - left: 0; - top: 38px; + &::after { + content: ' '; + display: block; + animation: blockTextShine 1s linear infinite forwards; + background-repeat: no-repeat; + background-size: cover; + background-image: linear-gradient( + to right, + $theme-gray-100 0%, + $theme-gray-50 20%, + $theme-gray-100 40%, + $theme-gray-100 100% + ); + height: 10px; + } } +} - .skeleton-line-5 { - left: 200px; - top: 28px; - height: 10px; - } +$skeleton-line-widths: ( + 156px, + 235px, + 200px, +); - .skeleton-line-6 { - top: 14px; - left: 230px; - height: 10px; +@for $count from 1 through length($skeleton-line-widths) { + .skeleton-line-#{$count} { + width: nth($skeleton-line-widths, $count); } } diff --git a/app/assets/stylesheets/framework/banner.scss b/app/assets/stylesheets/framework/banner.scss index 6433b0c7855..02f3896d591 100644 --- a/app/assets/stylesheets/framework/banner.scss +++ b/app/assets/stylesheets/framework/banner.scss @@ -1,7 +1,7 @@ .banner-callout { display: flex; position: relative; - flex-wrap: wrap; + align-items: start; .banner-close { position: absolute; @@ -16,10 +16,25 @@ } .banner-graphic { - margin: 20px auto; + margin: 0 $gl-padding $gl-padding 0; } &.banner-non-empty-state { border-bottom: 1px solid $border-color; } + + @media (max-width: $screen-xs-max) { + justify-content: center; + flex-direction: column; + align-items: center; + + .banner-title, + .banner-buttons { + text-align: center; + } + + .banner-graphic { + margin-left: $gl-padding; + } + } } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 0be86c36738..f4f5926e198 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -422,25 +422,43 @@ } } -.btn-link.btn-secondary-hover-link { - color: $gl-text-color-secondary; +.btn-link { + padding: 0; + background-color: transparent; + color: $blue-600; + font-weight: normal; + border-radius: 0; + border-color: transparent; &:hover, &:active, &:focus { - color: $gl-link-color; - text-decoration: none; + color: $blue-800; + text-decoration: underline; + background-color: transparent; + border-color: transparent; } -} -.btn-link.btn-primary-hover-link { - color: inherit; + &.btn-secondary-hover-link { + color: $gl-text-color-secondary; - &:hover, - &:active, - &:focus { - color: $gl-link-color; - text-decoration: none; + &:hover, + &:active, + &:focus { + color: $gl-link-color; + text-decoration: none; + } + } + + &.btn-primary-hover-link { + color: inherit; + + &:hover, + &:active, + &:focus { + color: $gl-link-color; + text-decoration: none; + } } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index cc74cb72795..cc5fac6816d 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -481,7 +481,8 @@ .dropdown-menu-selectable { li { - a { + a, + button { padding: 8px 40px; position: relative; diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss index 30c15c231d5..606d4675f19 100644 --- a/app/assets/stylesheets/framework/snippets.scss +++ b/app/assets/stylesheets/framework/snippets.scss @@ -29,8 +29,10 @@ } .snippet-title { - font-size: 24px; + color: $gl-text-color; + font-size: 2em; font-weight: $gl-font-weight-bold; + min-height: $header-height; } .snippet-edited-ago { @@ -46,3 +48,26 @@ .snippet-scope-menu .btn-new { margin-top: 15px; } + +.snippet-embed-input { + height: 35px; +} + +.embed-snippet { + padding-right: 0; + padding-top: $gl-padding; + + .form-control { + cursor: auto; + width: 101%; + margin-left: -1px; + } + + .embed-toggle-list li button { + padding: 8px 40px; + } + + .embed-toggle { + height: 35px; + } +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 8ee1bb03d55..37223175199 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -714,20 +714,6 @@ $color-average-score: $orange-400; $color-low-score: $red-400; /* -Repo editor -*/ -$repo-editor-grey: #f6f7f9; -$repo-editor-grey-darker: #e9ebee; -$repo-editor-linear-gradient: linear-gradient( - to right, - $repo-editor-grey 0%, - $repo-editor-grey-darker, - 20%, - $repo-editor-grey 40%, - $repo-editor-grey 100% -); - -/* Performance Bar */ $perf-bar-text: #999; diff --git a/app/assets/stylesheets/highlight/embedded.scss b/app/assets/stylesheets/highlight/embedded.scss new file mode 100644 index 00000000000..44c8a1d39ec --- /dev/null +++ b/app/assets/stylesheets/highlight/embedded.scss @@ -0,0 +1,3 @@ +.code { + @import "white_base"; +} diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index c3d8f0c61a2..355c8d223f7 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -1,292 +1,3 @@ -/* https://github.com/aahan/pygments-github-style */ - -/* -* White Syntax Colors -*/ -$white-code-color: $gl-text-color; -$white-highlight: #fafe3d; -$white-pre-hll-bg: #f8eec7; -$white-hll-bg: #f8f8f8; -$white-over-bg: #ded7fc; -$white-expanded-border: #e0e0e0; -$white-expanded-bg: #f7f7f7; -$white-c: #998; -$white-err: #a61717; -$white-err-bg: #e3d2d2; -$white-cm: #998; -$white-cp: #999; -$white-c1: #998; -$white-cs: #999; -$white-gd: $black; -$white-gd-bg: #fdd; -$white-gd-x: $black; -$white-gd-x-bg: #faa; -$white-gr: #a00; -$white-gh: #999; -$white-gi: $black; -$white-gi-bg: #dfd; -$white-gi-x: $black; -$white-gi-x-bg: #afa; -$white-go: #888; -$white-gp: #555; -$white-gu: #800080; -$white-gt: #a00; -$white-kt: #458; -$white-m: #099; -$white-s: #d14; -$white-n: #333; -$white-na: teal; -$white-nb: #0086b3; -$white-nc: #458; -$white-no: teal; -$white-ni: purple; -$white-ne: #900; -$white-nf: #900; -$white-nn: #555; -$white-nt: navy; -$white-nv: teal; -$white-w: #bbb; -$white-mf: #099; -$white-mh: #099; -$white-mi: #099; -$white-mo: #099; -$white-sb: #d14; -$white-sc: #d14; -$white-sd: #d14; -$white-s2: #d14; -$white-se: #d14; -$white-sh: #d14; -$white-si: #d14; -$white-sx: #d14; -$white-sr: #009926; -$white-s1: #d14; -$white-ss: #990073; -$white-bp: #999; -$white-vc: teal; -$white-vg: teal; -$white-vi: teal; -$white-il: #099; -$white-gc-color: #999; -$white-gc-bg: #eaf2f5; - - -@mixin matchLine { - color: $black-transparent; - background-color: $gray-light; -} - .code.white { - // Line numbers - .line-numbers, - .diff-line-num { - background-color: $gray-light; - } - - .diff-line-num, - .diff-line-num a { - color: $black-transparent; - } - - // Code itself - pre.code, - .diff-line-num { - border-color: $white-normal; - } - - &, - pre.code, - .line_holder .line_content { - background-color: $white-light; - color: $white-code-color; - } - - // Diff line - .line_holder { - - &.match .line_content { - @include matchLine; - } - - .diff-line-num { - &.old { - background-color: $line-number-old; - border-color: $line-removed-dark; - - a { - color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%); - } - } - - &.new { - background-color: $line-number-new; - border-color: $line-added-dark; - - a { - color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%); - } - } - - &.is-over, - &.hll:not(.empty-cell).is-over { - background-color: $white-over-bg; - border-color: darken($white-over-bg, 5%); - - a { - color: darken($white-over-bg, 15%); - } - } - - &.hll:not(.empty-cell) { - background-color: $line-number-select; - border-color: $line-select-yellow-dark; - } - } - - &:not(.diff-expanded) + .diff-expanded, - &.diff-expanded + .line_holder:not(.diff-expanded) { - > .diff-line-num, - > .line_content { - border-top: 1px solid $white-expanded-border; - } - } - - &.diff-expanded { - > .diff-line-num, - > .line_content { - background: $white-expanded-bg; - border-color: $white-expanded-bg; - } - } - - .line_content { - &.old { - background-color: $line-removed; - - &::before { - color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%); - } - - span.idiff { - background-color: $line-removed-dark; - } - } - - &.new { - background-color: $line-added; - - &::before { - color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%); - } - - span.idiff { - background-color: $line-added-dark; - } - } - - &.match { - @include matchLine; - } - - &.hll:not(.empty-cell) { - background-color: $line-select-yellow; - } - } - } - - // highlight line via anchor - pre .hll { - background-color: $white-pre-hll-bg !important; - } - - // Search result highlight - span.highlight_word { - background-color: $white-highlight !important; - } - - // Links to URLs, emails, or dependencies - .line a { - color: $white-nb; - } - - .hll { background-color: $white-hll-bg; } - .c { color: $white-c; font-style: italic; } - .err { color: $white-err; background-color: $white-err-bg; } - .k { font-weight: $gl-font-weight-bold; } - .o { font-weight: $gl-font-weight-bold; } - .cm { color: $white-cm; font-style: italic; } - .cp { color: $white-cp; font-weight: $gl-font-weight-bold; } - .c1 { color: $white-c1; font-style: italic; } - .cs { color: $white-cs; font-weight: $gl-font-weight-bold; font-style: italic; } - - .gd { - color: $white-gd; - background-color: $white-gd-bg; - - .x { - color: $white-gd-x; - background-color: $white-gd-x-bg; - } - } - - .ge { font-style: italic; } - .gr { color: $white-gr; } - .gh { color: $white-gh; } - - .gi { - color: $white-gi; - background-color: $white-gi-bg; - - .x { - color: $white-gi-x; - background-color: $white-gi-x-bg; - } - } - - .go { color: $white-go; } - .gp { color: $white-gp; } - .gs { font-weight: $gl-font-weight-bold; } - .gu { color: $white-gu; font-weight: $gl-font-weight-bold; } - .gt { color: $white-gt; } - .kc { font-weight: $gl-font-weight-bold; } - .kd { font-weight: $gl-font-weight-bold; } - .kn { font-weight: $gl-font-weight-bold; } - .kp { font-weight: $gl-font-weight-bold; } - .kr { font-weight: $gl-font-weight-bold; } - .kt { color: $white-kt; font-weight: $gl-font-weight-bold; } - .m { color: $white-m; } - .s { color: $white-s; } - .n { color: $white-n; } - .na { color: $white-na; } - .nb { color: $white-nb; } - .nc { color: $white-nc; font-weight: $gl-font-weight-bold; } - .no { color: $white-no; } - .ni { color: $white-ni; } - .ne { color: $white-ne; font-weight: $gl-font-weight-bold; } - .nf { color: $white-nf; font-weight: $gl-font-weight-bold; } - .nn { color: $white-nn; } - .nt { color: $white-nt; } - .nv { color: $white-nv; } - .ow { font-weight: $gl-font-weight-bold; } - .w { color: $white-w; } - .mf { color: $white-mf; } - .mh { color: $white-mh; } - .mi { color: $white-mi; } - .mo { color: $white-mo; } - .sb { color: $white-sb; } - .sc { color: $white-sc; } - .sd { color: $white-sd; } - .s2 { color: $white-s2; } - .se { color: $white-se; } - .sh { color: $white-sh; } - .si { color: $white-si; } - .sx { color: $white-sx; } - .sr { color: $white-sr; } - .s1 { color: $white-s1; } - .ss { color: $white-ss; } - .bp { color: $white-bp; } - .vc { color: $white-vc; } - .vg { color: $white-vg; } - .vi { color: $white-vi; } - .il { color: $white-il; } - .gc { color: $white-gc-color; background-color: $white-gc-bg; } + @import "white_base"; } diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss new file mode 100644 index 00000000000..8cc5252648d --- /dev/null +++ b/app/assets/stylesheets/highlight/white_base.scss @@ -0,0 +1,290 @@ +/* https://github.com/aahan/pygments-github-style */ + +/* +* White Syntax Colors +*/ +$white-code-color: $gl-text-color; +$white-highlight: #fafe3d; +$white-pre-hll-bg: #f8eec7; +$white-hll-bg: #f8f8f8; +$white-over-bg: #ded7fc; +$white-expanded-border: #e0e0e0; +$white-expanded-bg: #f7f7f7; +$white-c: #998; +$white-err: #a61717; +$white-err-bg: #e3d2d2; +$white-cm: #998; +$white-cp: #999; +$white-c1: #998; +$white-cs: #999; +$white-gd: $black; +$white-gd-bg: #fdd; +$white-gd-x: $black; +$white-gd-x-bg: #faa; +$white-gr: #a00; +$white-gh: #999; +$white-gi: $black; +$white-gi-bg: #dfd; +$white-gi-x: $black; +$white-gi-x-bg: #afa; +$white-go: #888; +$white-gp: #555; +$white-gu: #800080; +$white-gt: #a00; +$white-kt: #458; +$white-m: #099; +$white-s: #d14; +$white-n: #333; +$white-na: teal; +$white-nb: #0086b3; +$white-nc: #458; +$white-no: teal; +$white-ni: purple; +$white-ne: #900; +$white-nf: #900; +$white-nn: #555; +$white-nt: navy; +$white-nv: teal; +$white-w: #bbb; +$white-mf: #099; +$white-mh: #099; +$white-mi: #099; +$white-mo: #099; +$white-sb: #d14; +$white-sc: #d14; +$white-sd: #d14; +$white-s2: #d14; +$white-se: #d14; +$white-sh: #d14; +$white-si: #d14; +$white-sx: #d14; +$white-sr: #009926; +$white-s1: #d14; +$white-ss: #990073; +$white-bp: #999; +$white-vc: teal; +$white-vg: teal; +$white-vi: teal; +$white-il: #099; +$white-gc-color: #999; +$white-gc-bg: #eaf2f5; + + +@mixin matchLine { + color: $black-transparent; + background-color: $gray-light; +} + + // Line numbers +.line-numbers, +.diff-line-num { + background-color: $gray-light; +} + +.diff-line-num, +.diff-line-num a { + color: $black-transparent; +} + +// Code itself +pre.code, +.diff-line-num { + border-color: $white-normal; +} + +&, +pre.code, +.line_holder .line_content { + background-color: $white-light; + color: $white-code-color; +} + +// Diff line +.line_holder { + + &.match .line_content { + @include matchLine; + } + + .diff-line-num { + &.old { + background-color: $line-number-old; + border-color: $line-removed-dark; + + a { + color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%); + } + } + + &.new { + background-color: $line-number-new; + border-color: $line-added-dark; + + a { + color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%); + } + } + + &.is-over, + &.hll:not(.empty-cell).is-over { + background-color: $white-over-bg; + border-color: darken($white-over-bg, 5%); + + a { + color: darken($white-over-bg, 15%); + } + } + + &.hll:not(.empty-cell) { + background-color: $line-number-select; + border-color: $line-select-yellow-dark; + } + } + + &:not(.diff-expanded) + .diff-expanded, + &.diff-expanded + .line_holder:not(.diff-expanded) { + > .diff-line-num, + > .line_content { + border-top: 1px solid $white-expanded-border; + } + } + + &.diff-expanded { + > .diff-line-num, + > .line_content { + background: $white-expanded-bg; + border-color: $white-expanded-bg; + } + } + + .line_content { + &.old { + background-color: $line-removed; + + &::before { + color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%); + } + + span.idiff { + background-color: $line-removed-dark; + } + } + + &.new { + background-color: $line-added; + + &::before { + color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%); + } + + span.idiff { + background-color: $line-added-dark; + } + } + + &.match { + @include matchLine; + } + + &.hll:not(.empty-cell) { + background-color: $line-select-yellow; + } + } +} + +// highlight line via anchor +pre .hll { + background-color: $white-pre-hll-bg !important; +} + + // Search result highlight +span.highlight_word { + background-color: $white-highlight !important; +} + + // Links to URLs, emails, or dependencies +.line a { + color: $white-nb; +} + +.hll { background-color: $white-hll-bg; } +.c { color: $white-c; font-style: italic; } +.err { color: $white-err; background-color: $white-err-bg; } +.k { font-weight: $gl-font-weight-bold; } +.o { font-weight: $gl-font-weight-bold; } +.cm { color: $white-cm; font-style: italic; } +.cp { color: $white-cp; font-weight: $gl-font-weight-bold; } +.c1 { color: $white-c1; font-style: italic; } +.cs { color: $white-cs; font-weight: $gl-font-weight-bold; font-style: italic; } + +.gd { + color: $white-gd; + background-color: $white-gd-bg; + + .x { + color: $white-gd-x; + background-color: $white-gd-x-bg; + } +} + +.ge { font-style: italic; } +.gr { color: $white-gr; } +.gh { color: $white-gh; } + +.gi { + color: $white-gi; + background-color: $white-gi-bg; + + .x { + color: $white-gi-x; + background-color: $white-gi-x-bg; + } +} + +.go { color: $white-go; } +.gp { color: $white-gp; } +.gs { font-weight: $gl-font-weight-bold; } +.gu { color: $white-gu; font-weight: $gl-font-weight-bold; } +.gt { color: $white-gt; } +.kc { font-weight: $gl-font-weight-bold; } +.kd { font-weight: $gl-font-weight-bold; } +.kn { font-weight: $gl-font-weight-bold; } +.kp { font-weight: $gl-font-weight-bold; } +.kr { font-weight: $gl-font-weight-bold; } +.kt { color: $white-kt; font-weight: $gl-font-weight-bold; } +.m { color: $white-m; } +.s { color: $white-s; } +.n { color: $white-n; } +.na { color: $white-na; } +.nb { color: $white-nb; } +.nc { color: $white-nc; font-weight: $gl-font-weight-bold; } +.no { color: $white-no; } +.ni { color: $white-ni; } +.ne { color: $white-ne; font-weight: $gl-font-weight-bold; } +.nf { color: $white-nf; font-weight: $gl-font-weight-bold; } +.nn { color: $white-nn; } +.nt { color: $white-nt; } +.nv { color: $white-nv; } +.ow { font-weight: $gl-font-weight-bold; } +.w { color: $white-w; } +.mf { color: $white-mf; } +.mh { color: $white-mh; } +.mi { color: $white-mi; } +.mo { color: $white-mo; } +.sb { color: $white-sb; } +.sc { color: $white-sc; } +.sd { color: $white-sd; } +.s2 { color: $white-s2; } +.se { color: $white-se; } +.sh { color: $white-sh; } +.si { color: $white-si; } +.sx { color: $white-sx; } +.sr { color: $white-sr; } +.s1 { color: $white-s1; } +.ss { color: $white-ss; } +.bp { color: $white-bp; } +.vc { color: $white-vc; } +.vg { color: $white-vg; } +.vi { color: $white-vi; } +.il { color: $white-il; } +.gc { color: $white-gc-color; background-color: $white-gc-bg; } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 36ca2dd87fa..11052be40a8 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -818,7 +818,6 @@ } .discussion-notes { - padding: 0 $gl-padding $gl-padding; min-height: 35px; &:first-child { diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index e5afa8fffcb..3af8d80daab 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -194,3 +194,38 @@ .issuable-row { background-color: $white-light; } + +.milestone-deprecation-message { + .popover { + padding: 0; + } + + .popover-content { + padding: 0; + } +} + +.milestone-popover-body { + padding: $gl-padding-8; + background-color: $gray-light; +} + +.milestone-popover-footer { + padding: $gl-padding-8 $gl-padding; + border-top: 1px solid $white-dark; +} + +.milestone-popover-instructions-list { + padding-left: 2em; + + > li { + padding-left: 1em; + } +} + +@media (max-width: $screen-xs-max) { + .milestone-banner-text, + .milestone-banner-link { + display: inline; + } +} diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 8720f821ce9..4a528bc2bb1 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -173,7 +173,11 @@ } .discussion-form { - padding-top: $gl-padding-top; + background-color: $white-light; +} + +.discussion-form-container { + padding: $gl-padding-top $gl-padding $gl-padding; } .discussion-notes .disabled-comment { @@ -233,7 +237,12 @@ .discussion-body, .diff-file { .discussion-reply-holder { - padding-top: $gl-padding; + background-color: $white-light; + padding: 10px 16px; + + &.is-replying { + padding-bottom: $gl-padding; + } } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 9d9cbecc958..81e98f358a8 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -47,7 +47,7 @@ ul.notes { } .timeline-entry-inner { - padding: $gl-padding 0; + padding: $gl-padding $gl-btn-padding; border-bottom: 1px solid $white-normal; } @@ -94,6 +94,12 @@ ul.notes { } } + &.note-discussion { + .timeline-entry-inner { + padding: $gl-padding 10px; + } + } + .editing-spinner { display: none; } @@ -346,8 +352,6 @@ ul.notes { } .discussion-notes { - background-color: $white-light; - &:not(:first-child) { border-top: 1px solid $white-normal; margin-top: 20px; @@ -359,6 +363,10 @@ ul.notes { } } + .notes { + background-color: $white-light; + } + a code { top: 0; margin-right: 0; @@ -639,6 +647,8 @@ ul.notes { border-bottom: 1px solid $white-normal; .timeline-entry-inner { + padding-left: $gl-padding; + padding-right: $gl-padding; border-bottom: 0; } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 8d5eb2e8c5a..855ebf7d86d 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -14,6 +14,11 @@ .commit-title { margin: 0; + white-space: normal; + + @media (max-width: $screen-sm-max) { + justify-content: flex-end; + } } .ci-table { @@ -344,7 +349,6 @@ svg { vertical-align: middle; - margin-right: 3px; } .stage-column { @@ -495,17 +499,12 @@ svg { fill: $gl-text-color-secondary; position: relative; - left: 1px; top: -1px; - width: 16px; - height: 16px; } &.play { svg { - width: 16px; - height: 16px; - left: 3px; + left: 2px; } } } diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index a414deb8921..5f46e69a56d 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -429,6 +429,7 @@ .projects-sidebar { display: flex; flex-direction: column; + height: 100%; .context-header { width: auto; @@ -438,8 +439,8 @@ .multi-file-commit-panel-inner { display: flex; - flex: 1; flex-direction: column; + height: 100%; } .multi-file-commit-panel-inner-scroll { diff --git a/app/assets/stylesheets/snippets.scss b/app/assets/stylesheets/snippets.scss new file mode 100644 index 00000000000..0d6b0735f70 --- /dev/null +++ b/app/assets/stylesheets/snippets.scss @@ -0,0 +1,156 @@ +@import "framework/variables"; + +.gitlab-embed-snippets { + @import "highlight/embedded"; + @import "framework/images"; + + $border-style: 1px solid $border-color; + + font-family: $regular_font; + font-size: $gl-font-size; + line-height: $code_line_height; + color: $gl-text-color; + margin: 20px; + font-weight: 200; + + .gl-snippet-icon { + display: inline-block; + background: url(asset_path('ext_snippet_icons/ext_snippet_icons.png')) no-repeat; + overflow: hidden; + text-align: left; + width: 16px; + height: 16px; + background-size: cover; + + &.gl-snippet-icon-doc_code { background-position: 0 0; } + &.gl-snippet-icon-doc_text { background-position: 0 -16px; } + &.gl-snippet-icon-download { background-position: 0 -32px; } + } + + .blob-viewer { + background-color: $white-light; + text-align: left; + } + + .file-content.code { + border: $border-style; + border-radius: 0 0 4px 4px; + display: flex; + box-shadow: none; + margin: 0; + padding: 0; + table-layout: fixed; + + .blob-content { + overflow-x: auto; + + pre { + padding: 10px; + border: 0; + border-radius: 0; + font-family: $monospace_font; + font-size: $code_font_size; + line-height: $code_line_height; + margin: 0; + overflow: auto; + overflow-y: hidden; + white-space: pre; + word-wrap: normal; + border-left: $border-style; + } + } + + .line-numbers { + padding: 10px; + text-align: right; + float: left; + + .diff-line-num { + font-family: $monospace_font; + display: block; + font-size: $code_font_size; + min-height: $code_line_height; + white-space: nowrap; + color: $black-transparent; + min-width: 30px; + } + + .diff-line-num:hover { + color: $almost-black; + cursor: pointer; + } + } + } + + .file-title-flex-parent { + display: flex; + align-items: center; + justify-content: space-between; + background-color: $gray-light; + border: $border-style; + border-bottom: 0; + padding: $gl-padding-top $gl-padding; + margin: 0; + border-radius: $border-radius-default $border-radius-default 0 0; + + .file-header-content { + .file-title-name { + font-weight: $gl-font-weight-bold; + } + + .gitlab-embedded-snippets-title { + text-decoration: none; + color: $gl-text-color; + + &:hover { + text-decoration: underline; + } + } + + .gitlab-logo { + display: inline-block; + padding-left: 5px; + text-decoration: none; + color: $gl-text-color-secondary; + + .logo-text { + background: image_url('ext_snippet_icons/logo.png') no-repeat left center; + background-size: 18px; + font-weight: $gl-font-weight-normal; + padding-left: 24px; + } + } + } + + img, + .gl-snippet-icon { + display: inline-block; + vertical-align: middle; + } + } + + .btn-group { + a.btn { + background-color: $white-light; + text-decoration: none; + padding: 7px 9px; + border: $border-style; + border-right: 0; + + &:hover { + background-color: $white-normal; + border-color: $border-white-normal; + text-decoration: none; + } + + &:first-child { + border-radius: 3px 0 0 3px; + } + + &:last-child { + border-radius: 0 3px 3px 0; + border-right: $border-style; + } + } + } +} diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 145f74d9e59..8958eab0423 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -57,22 +57,17 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController def application_setting_params params[:application_setting] ||= {} - import_sources = params[:application_setting][:import_sources] - if import_sources.nil? - params[:application_setting][:import_sources] = [] - else - import_sources.map! do |source| - source.to_str - end - end + if params[:application_setting].key?(:enabled_oauth_sign_in_sources) + enabled_oauth_sign_in_sources = params[:application_setting].delete(:enabled_oauth_sign_in_sources) + enabled_oauth_sign_in_sources&.delete("") - enabled_oauth_sign_in_sources = params[:application_setting].delete(:enabled_oauth_sign_in_sources) - - params[:application_setting][:disabled_oauth_sign_in_sources] = - AuthHelper.button_based_providers.map(&:to_s) - - Array(enabled_oauth_sign_in_sources) + params[:application_setting][:disabled_oauth_sign_in_sources] = + AuthHelper.button_based_providers.map(&:to_s) - + Array(enabled_oauth_sign_in_sources) + end + params[:application_setting][:import_sources]&.delete("") params[:application_setting][:restricted_visibility_levels]&.delete("") params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file] diff --git a/app/controllers/concerns/checks_collaboration.rb b/app/controllers/concerns/checks_collaboration.rb new file mode 100644 index 00000000000..81367663a06 --- /dev/null +++ b/app/controllers/concerns/checks_collaboration.rb @@ -0,0 +1,21 @@ +module ChecksCollaboration + def can_collaborate_with_project?(project, ref: nil) + return true if can?(current_user, :push_code, project) + + can_create_merge_request = + can?(current_user, :create_merge_request_in, project) && + current_user.already_forked?(project) + + can_create_merge_request || + user_access(project).can_push_to_branch?(ref) + end + + # rubocop:disable Gitlab/ModuleWithInstanceVariables + # enabling this so we can easily cache the user access value as it might be + # used across multiple calls in the view + def user_access(project) + @user_access ||= {} + @user_access[project] ||= Gitlab::UserAccess.new(current_user, project: project) + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables +end diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb index 9095cc7f783..120614739aa 100644 --- a/app/controllers/concerns/snippets_actions.rb +++ b/app/controllers/concerns/snippets_actions.rb @@ -17,6 +17,10 @@ module SnippetsActions end # rubocop:enable Gitlab/ModuleWithInstanceVariables + def js_request? + request.format.js? + end + private def convert_line_endings(content) diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 6d9b42a2c04..032bb2267e7 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -1,5 +1,6 @@ class Projects::ApplicationController < ApplicationController include RoutableActions + include ChecksCollaboration skip_before_action :authenticate_user! before_action :project @@ -31,14 +32,6 @@ class Projects::ApplicationController < ApplicationController @repository ||= project.repository end - def can_collaborate_with_project?(project = nil, ref: nil) - project ||= @project - - can?(current_user, :push_code, project) || - (current_user && current_user.already_forked?(project)) || - user_access(project).can_push_to_branch?(ref) - end - def authorize_action!(action) unless can?(current_user, action, project) return access_denied! @@ -91,9 +84,4 @@ class Projects::ApplicationController < ApplicationController def check_issues_available! return render_404 unless @project.feature_available?(:issues, current_user) end - - def user_access(project) - @user_access ||= {} - @user_access[project] ||= Gitlab::UserAccess.new(current_user, project: project) - end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index b14939c4216..767e492f566 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -20,7 +20,7 @@ class Projects::IssuesController < Projects::ApplicationController before_action :authorize_update_issuable!, 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] + before_action :authorize_create_merge_request_from!, only: [:create_merge_request] respond_to :html diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index a90030a8312..4a377fefc62 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -5,7 +5,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap skip_before_action :merge_request before_action :whitelist_query_limiting, only: [:create] - before_action :authorize_create_merge_request! + before_action :authorize_create_merge_request_from! before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path] before_action :build_merge_request, except: [:create] diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 7c19aa7bb23..208a1d19862 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -5,6 +5,8 @@ class Projects::SnippetsController < Projects::ApplicationController include SnippetsActions include RendersBlob + skip_before_action :verify_authenticity_token, only: [:show], if: :js_request? + before_action :check_snippets_available! before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam] @@ -71,6 +73,7 @@ class Projects::SnippetsController < Projects::ApplicationController format.json do render_blob_json(blob) end + format.js { render 'shared/snippets/show'} end end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index be2d3f638ff..3d51520ddf4 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -6,6 +6,8 @@ class SnippetsController < ApplicationController include RendersBlob include PreviewMarkdown + skip_before_action :verify_authenticity_token, only: [:show], if: :js_request? + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] # Allow read snippet @@ -77,6 +79,8 @@ class SnippetsController < ApplicationController format.json do render_blob_json(blob) end + + format.js { render 'shared/snippets/show' } end end diff --git a/app/finders/merge_request_target_project_finder.rb b/app/finders/merge_request_target_project_finder.rb index f358938344e..188ec447a94 100644 --- a/app/finders/merge_request_target_project_finder.rb +++ b/app/finders/merge_request_target_project_finder.rb @@ -12,6 +12,7 @@ class MergeRequestTargetProjectFinder if @source_project.fork_network @source_project.fork_network.projects .public_or_visible_to_user(current_user) + .non_archived .with_feature_available_for_user(:merge_requests, current_user) else Project.where(id: source_project) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index b3b080e6dcf..3fbb32c5229 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -74,10 +74,12 @@ module ApplicationSettingsHelper css_class = 'btn' css_class << ' active' unless disabled checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]' + name = Gitlab::Auth::OAuth::Provider.label_for(source) label_tag(checkbox_name, class: css_class) do check_box_tag(checkbox_name, source, !disabled, - autocomplete: 'off') + Gitlab::Auth::OAuth::Provider.label_for(source) + autocomplete: 'off', + id: name.tr(' ', '_')) + name end end end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 2b440e4d584..866b8773db6 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -59,7 +59,7 @@ module BlobHelper button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' } elsif can_modify_blob?(blob, project, ref) button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' - elsif can?(current_user, :fork_project, project) + elsif can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project) edit_fork_button_tag(common_classes, project, label, edit_modify_file_fork_params(action), action) end end @@ -280,7 +280,7 @@ module BlobHelper options << link_to("submit an issue", new_project_issue_path(project)) end - merge_project = can?(current_user, :create_merge_request, project) ? project : (current_user && current_user.fork_of(project)) + merge_project = merge_request_source_project_for_project(@project) if merge_project options << link_to("create a merge request", project_new_merge_request_path(project)) end @@ -334,7 +334,7 @@ module BlobHelper # Web IDE (Beta) requires the user to have this feature enabled elsif !current_user || (current_user && can_modify_blob?(blob, project, ref)) edit_link_tag(text, edit_path, common_classes) - elsif current_user && can?(current_user, :fork_project, project) + elsif can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project) edit_fork_button_tag(common_classes, project, text, edit_blob_fork_params(edit_path)) end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 7cc56de24e4..98894b86551 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -163,7 +163,7 @@ module CommitsHelper tooltip = "#{action.capitalize} this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip btn_class = "btn btn-#{btn_class}" unless btn_class.nil? - if can_collaborate_with_project? + if can_collaborate_with_project?(@project) link_to action.capitalize, "#modal-#{action}-commit", 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" elsif can?(current_user, :fork_project, @project) continue_params = { diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb index 8bf96c0905f..2df5b5d1695 100644 --- a/app/helpers/compare_helper.rb +++ b/app/helpers/compare_helper.rb @@ -3,7 +3,7 @@ module CompareHelper from.present? && to.present? && from != to && - can?(current_user, :create_merge_request, project) && + can?(current_user, :create_merge_request_from, project) && project.repository.branch_exists?(from) && project.repository.branch_exists?(to) end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index c5522ff7a69..2f304b040c7 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -43,6 +43,10 @@ module IconsHelper content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes) end + def external_snippet_icon(name) + content_tag(:span, "", class: "gl-snippet-icon gl-snippet-icon-#{name}") + end + def audit_icon(names, options = {}) case names when "standard" diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 0f25d401406..96dc7ae1185 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -82,8 +82,8 @@ module IssuesHelper names.to_sentence end - def award_state_class(awards, current_user) - if !current_user + def award_state_class(awardable, awards, current_user) + if !can?(current_user, :award_emoji, awardable) "disabled" elsif current_user && awards.find { |a| a.user_id == current_user.id } "active" @@ -126,6 +126,17 @@ module IssuesHelper link_to link_text, path end + def show_new_issue_link?(project) + return false unless project + return false if project.archived? + + # We want to show the link to users that are not signed in, that way they + # get directed to the sign-in/sign-up flow and afterwards to the new issue page. + return true unless current_user + + can?(current_user, :create_issue, project) + end + # Required for Banzai::Filter::IssueReferenceFilter module_function :url_for_issue module_function :url_for_internal_issue diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index fb4fe1c40b7..c19c5b9cc82 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -138,6 +138,18 @@ module MergeRequestsHelper end end + def merge_request_source_project_for_project(project = @project) + unless can?(current_user, :create_merge_request_in, project) + return nil + end + + if can?(current_user, :create_merge_request_from, project) + project + else + current_user.fork_of(project) + end + end + def merge_params_ee(merge_request) {} end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 27ed48fdbc7..7f67574a428 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -6,10 +6,6 @@ module NotesHelper end end - def note_editable?(note) - Ability.can_edit_note?(current_user, note) - end - def note_supports_quick_actions?(note) Notes::QuickActionsService.supported?(note) end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 15f48e43a28..a64b2acdd77 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -157,40 +157,6 @@ module ProjectsHelper current_user&.recent_push(@project) end - def project_feature_access_select(field) - # Don't show option "everyone with access" if project is private - options = project_feature_options - - level = @project.project_feature.public_send(field) # rubocop:disable GitlabSecurity/PublicSend - - if @project.private? - disabled_option = ProjectFeature::ENABLED - highest_available_option = ProjectFeature::PRIVATE if level == disabled_option - end - - options = options_for_select( - options.invert, - selected: highest_available_option || level, - disabled: disabled_option - ) - - content_tag :div, class: "select-wrapper" do - concat( - content_tag( - :select, - options, - name: "project[project_feature_attributes][#{field}]", - id: "project_project_feature_attributes_#{field}", - class: "pull-right form-control select-control #{repo_children_classes(field)} ", - data: { field: field } - ) - ) - concat( - icon('chevron-down') - ) - end.html_safe - end - def link_to_autodeploy_doc link_to _('About auto deploy'), help_page_path('ci/autodeploy/index'), target: '_blank' end @@ -274,16 +240,6 @@ module ProjectsHelper private - def repo_children_classes(field) - needs_repo_check = [:merge_requests_access_level, :builds_access_level] - return unless needs_repo_check.include?(field) - - classes = "project-repo-select js-repo-select" - classes << " disabled" unless @project.feature_available?(:repository, current_user) - - classes - end - def get_project_nav_tabs(project, current_user) nav_tabs = [:home] @@ -447,14 +403,6 @@ module ProjectsHelper filtered_message.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]") end - def project_feature_options - { - ProjectFeature::DISABLED => s_('ProjectFeature|Disabled'), - ProjectFeature::PRIVATE => s_('ProjectFeature|Only team members'), - ProjectFeature::ENABLED => s_('ProjectFeature|Everyone with access') - } - end - def project_child_container_class(view_path) view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}" end @@ -463,20 +411,6 @@ module ProjectsHelper IssuesFinder.new(current_user, project_id: project.id).execute end - def visibility_select_options(project, selected_level) - level_options = Gitlab::VisibilityLevel.values.each_with_object([]) do |level, level_options| - next if restricted_levels.include?(level) - - level_options << [ - visibility_level_label(level), - { data: { description: visibility_level_description(level, project) } }, - level - ] - end - - options_for_select(level_options, selected_level) - end - def restricted_levels return [] if current_user.admin? diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 00e7e4230b9..733832c1bbb 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -101,4 +101,39 @@ module SnippetsHelper # Return snippet with chunk array { snippet_object: snippet, snippet_chunks: snippet_chunks } end + + def snippet_embed + "<script src=\"#{url_for(only_path: false, overwrite_params: nil)}.js\"></script>" + end + + def embedded_snippet_raw_button + blob = @snippet.blob + return if blob.empty? || blob.raw_binary? || blob.stored_externally? + + snippet_raw_url = if @snippet.is_a?(PersonalSnippet) + raw_snippet_url(@snippet) + else + raw_project_snippet_url(@snippet.project, @snippet) + end + + link_to external_snippet_icon('doc_code'), snippet_raw_url, class: 'btn', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw' + end + + def embedded_snippet_download_button + download_url = if @snippet.is_a?(PersonalSnippet) + raw_snippet_url(@snippet, inline: false) + else + raw_project_snippet_url(@snippet.project, @snippet, inline: false) + end + + link_to external_snippet_icon('download'), download_url, class: 'btn', target: '_blank', title: 'Download', rel: 'noopener noreferrer' + end + + def public_snippet? + if @snippet.project_id? + can?(nil, :read_project_snippet, @snippet) + else + can?(nil, :read_personal_snippet, @snippet) + end + end end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 5e7c20ef51e..dc42caa70e5 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -90,7 +90,7 @@ module TreeHelper end def commit_in_single_accessible_branch - branch_name = html_escape(selected_branch) + branch_name = ERB::Util.html_escape(selected_branch) message = _("Your changes can be committed to %{branch_name} because a merge "\ "request is open.") % { branch_name: "<strong>#{branch_name}</strong>" } diff --git a/app/models/ability.rb b/app/models/ability.rb index 6dae49f38dc..618d4af4272 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -46,10 +46,6 @@ class Ability end end - def can_edit_note?(user, note) - allowed?(user, :edit_note, note) - end - def allowed?(user, action, subject = :global, opts = {}) if subject.is_a?(Hash) opts, subject = subject, :global diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb index 62d768cc6cf..44cb583e1bd 100644 --- a/app/models/ci/group_variable.rb +++ b/app/models/ci/group_variable.rb @@ -4,7 +4,7 @@ module Ci include HasVariable include Presentable - belongs_to :group + belongs_to :group, class_name: "::Group" alias_attribute :secret_value, :value diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index d8394415362..fce37e7f78e 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -79,11 +79,7 @@ module Awardable end def user_can_award?(current_user, name) - if user_authored?(current_user) - !awardable_votes?(normalize_name(name)) - else - true - end + awardable_by_user?(current_user, name) && Ability.allowed?(current_user, :award_emoji, self) end def user_authored?(current_user) @@ -119,4 +115,12 @@ module Awardable def normalize_name(name) Gitlab::Emoji.normalize_emoji_name(name) end + + def awardable_by_user?(current_user, name) + if user_authored?(current_user) + !awardable_votes?(normalize_name(name)) + else + true + end + end end diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb index cbec735c2dd..96a43006642 100644 --- a/app/models/internal_id.rb +++ b/app/models/internal_id.rb @@ -23,9 +23,12 @@ class InternalId < ActiveRecord::Base # # The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL). # As such, the increment is atomic and safe to be called concurrently. - def increment_and_save! + # + # If a `maximum_iid` is passed in, this overrides the incremented value if it's + # greater than that. This can be used to correct the increment value if necessary. + def increment_and_save!(maximum_iid) lock! - self.last_value = (last_value || 0) + 1 + self.last_value = [(last_value || 0) + 1, (maximum_iid || 0) + 1].max save! last_value end @@ -89,7 +92,16 @@ class InternalId < ActiveRecord::Base # and increment its last value # # Note this will acquire a ROW SHARE lock on the InternalId record - (lookup || create_record).increment_and_save! + + # Note we always calculate the maximum iid present here and + # pass it in to correct the InternalId entry if it's last_value is off. + # + # This can happen in a transition phase where both `AtomicInternalId` and + # `NonatomicInternalId` code runs (e.g. during a deploy). + # + # This is subject to be cleaned up with the 10.8 release: + # https://gitlab.com/gitlab-org/gitlab-ce/issues/45389. + (lookup || create_record).increment_and_save!(maximum_iid) end end @@ -115,11 +127,15 @@ class InternalId < ActiveRecord::Base InternalId.create!( **scope, usage: usage_value, - last_value: init.call(subject) || 0 + last_value: maximum_iid ) end rescue ActiveRecord::RecordNotUnique lookup end + + def maximum_iid + @maximum_iid ||= init.call(subject) || 0 + end end end diff --git a/app/models/repository.rb b/app/models/repository.rb index fd1afafe4df..5bdaa7f0720 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -331,6 +331,7 @@ class Repository return unless empty? expire_method_caches(%i(has_visible_content?)) + raw_repository.expire_has_local_branches_cache end def lookup_cache diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 1ab391a5a9d..808a81cbbf9 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -11,7 +11,7 @@ module Ci end condition(:owner_of_job) do - can?(:developer_access) && @subject.triggered_by?(@user) + @subject.triggered_by?(@user) end rule { protected_ref }.policy do @@ -19,6 +19,6 @@ module Ci prevent :erase_build end - rule { can?(:master_access) | owner_of_job }.enable :erase_build + rule { can?(:admin_build) | (can?(:update_build) & owner_of_job) }.enable :erase_build end end diff --git a/app/policies/ci/pipeline_schedule_policy.rb b/app/policies/ci/pipeline_schedule_policy.rb index dc7a4aed577..ecba0488d3c 100644 --- a/app/policies/ci/pipeline_schedule_policy.rb +++ b/app/policies/ci/pipeline_schedule_policy.rb @@ -7,23 +7,17 @@ module Ci end condition(:owner_of_schedule) do - can?(:developer_access) && pipeline_schedule.owned_by?(@user) + pipeline_schedule.owned_by?(@user) end - condition(:non_owner_of_schedule) do - !pipeline_schedule.owned_by?(@user) - end - - rule { can?(:developer_access) }.policy do - enable :play_pipeline_schedule - end + rule { can?(:create_pipeline) }.enable :play_pipeline_schedule - rule { can?(:master_access) | owner_of_schedule }.policy do + rule { can?(:admin_pipeline) | (can?(:update_build) & owner_of_schedule) }.policy do enable :update_pipeline_schedule enable :admin_pipeline_schedule end - rule { can?(:master_access) & non_owner_of_schedule }.policy do + rule { can?(:admin_pipeline_schedule) & ~owner_of_schedule }.policy do enable :take_ownership_pipeline_schedule end diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb index e86d1c8f98e..b431d376e3d 100644 --- a/app/policies/issuable_policy.rb +++ b/app/policies/issuable_policy.rb @@ -18,9 +18,7 @@ class IssuablePolicy < BasePolicy rule { locked & ~is_project_member }.policy do prevent :create_note - prevent :update_note prevent :admin_note prevent :resolve_note - prevent :edit_note end end diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb index d4cb5a77e63..077a6761ee6 100644 --- a/app/policies/note_policy.rb +++ b/app/policies/note_policy.rb @@ -1,26 +1,21 @@ class NotePolicy < BasePolicy delegate { @subject.project } - delegate { @subject.noteable if @subject.noteable.lockable? } + delegate { @subject.noteable if DeclarativePolicy.has_policy?(@subject.noteable) } condition(:is_author) { @user && @subject.author == @user } - condition(:for_merge_request, scope: :subject) { @subject.for_merge_request? } condition(:is_noteable_author) { @user && @subject.noteable.author_id == @user.id } condition(:editable, scope: :subject) { @subject.editable? } - rule { ~editable | anonymous }.prevent :edit_note - - rule { is_author | admin }.enable :edit_note - rule { can?(:master_access) }.enable :edit_note + rule { ~editable }.prevent :admin_note rule { is_author }.policy do enable :read_note - enable :update_note enable :admin_note enable :resolve_note end - rule { for_merge_request & is_noteable_author }.policy do + rule { is_noteable_author }.policy do enable :resolve_note end end diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb index cac0530b9f7..c1a84727cfa 100644 --- a/app/policies/personal_snippet_policy.rb +++ b/app/policies/personal_snippet_policy.rb @@ -25,4 +25,6 @@ class PersonalSnippetPolicy < BasePolicy end rule { anonymous }.prevent :comment_personal_snippet + + rule { can?(:comment_personal_snippet) }.enable :award_emoji end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 21bb0934dee..3529d0aa60c 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -1,12 +1,26 @@ class ProjectPolicy < BasePolicy - def self.create_read_update_admin(name) - [ - :"create_#{name}", - :"read_#{name}", - :"update_#{name}", - :"admin_#{name}" - ] - end + extend ClassMethods + + READONLY_FEATURES_WHEN_ARCHIVED = %i[ + issue + list + merge_request + label + milestone + project_snippet + wiki + note + pipeline + pipeline_schedule + build + trigger + environment + deployment + commit_status + container_image + pages + cluster + ].freeze desc "User is a project owner" condition :owner do @@ -15,7 +29,7 @@ class ProjectPolicy < BasePolicy end desc "Project has public builds enabled" - condition(:public_builds, scope: :subject) { project.public_builds? } + condition(:public_builds, scope: :subject, score: 0) { project.public_builds? } # For guest access we use #team_member? so we can use # project.members, which gets cached in subject scope. @@ -35,7 +49,7 @@ class ProjectPolicy < BasePolicy condition(:master) { team_access_level >= Gitlab::Access::MASTER } desc "Project is public" - condition(:public_project, scope: :subject) { project.public? } + condition(:public_project, scope: :subject, score: 0) { project.public? } desc "Project is visible to internal users" condition(:internal_access) do @@ -46,7 +60,7 @@ class ProjectPolicy < BasePolicy condition(:group_member, scope: :subject) { project_group_member? } desc "Project is archived" - condition(:archived, scope: :subject) { project.archived? } + condition(:archived, scope: :subject, score: 0) { project.archived? } condition(:default_issues_tracker, scope: :subject) { project.default_issues_tracker? } @@ -56,10 +70,10 @@ class ProjectPolicy < BasePolicy end desc "Project has an external wiki" - condition(:has_external_wiki, scope: :subject) { project.has_external_wiki? } + condition(:has_external_wiki, scope: :subject, score: 0) { project.has_external_wiki? } desc "Project has request access enabled" - condition(:request_access_enabled, scope: :subject) { project.request_access_enabled } + condition(:request_access_enabled, scope: :subject, score: 0) { project.request_access_enabled } desc "Has merge requests allowing pushes to user" condition(:has_merge_requests_allowing_pushes, scope: :subject) do @@ -126,6 +140,7 @@ class ProjectPolicy < BasePolicy rule { can?(:guest_access) }.policy do enable :read_project + enable :create_merge_request_in enable :read_board enable :read_list enable :read_wiki @@ -140,6 +155,7 @@ class ProjectPolicy < BasePolicy enable :create_note enable :upload_file enable :read_cycle_analytics + enable :award_emoji end # These abilities are not allowed to admins that are not members of the project, @@ -197,7 +213,7 @@ class ProjectPolicy < BasePolicy enable :create_pipeline enable :update_pipeline enable :create_pipeline_schedule - enable :create_merge_request + enable :create_merge_request_from enable :create_wiki enable :push_code enable :resolve_note @@ -208,7 +224,7 @@ class ProjectPolicy < BasePolicy end rule { can?(:master_access) }.policy do - enable :delete_protected_branch + enable :push_to_delete_protected_branch enable :update_project_snippet enable :update_environment enable :update_deployment @@ -231,37 +247,50 @@ class ProjectPolicy < BasePolicy end rule { archived }.policy do - prevent :create_merge_request prevent :push_code - prevent :delete_protected_branch - prevent :update_merge_request - prevent :admin_merge_request + prevent :push_to_delete_protected_branch + prevent :request_access + prevent :upload_file + prevent :resolve_note + prevent :create_merge_request_from + prevent :create_merge_request_in + prevent :award_emoji + + READONLY_FEATURES_WHEN_ARCHIVED.each do |feature| + prevent(*create_update_admin_destroy(feature)) + end + end + + rule { issues_disabled }.policy do + prevent(*create_read_update_admin_destroy(:issue)) end rule { merge_requests_disabled | repository_disabled }.policy do - prevent(*create_read_update_admin(:merge_request)) + prevent :create_merge_request_in + prevent :create_merge_request_from + prevent(*create_read_update_admin_destroy(:merge_request)) end rule { issues_disabled & merge_requests_disabled }.policy do - prevent(*create_read_update_admin(:label)) - prevent(*create_read_update_admin(:milestone)) + prevent(*create_read_update_admin_destroy(:label)) + prevent(*create_read_update_admin_destroy(:milestone)) end rule { snippets_disabled }.policy do - prevent(*create_read_update_admin(:project_snippet)) + prevent(*create_read_update_admin_destroy(:project_snippet)) end rule { wiki_disabled & ~has_external_wiki }.policy do - prevent(*create_read_update_admin(:wiki)) + prevent(*create_read_update_admin_destroy(:wiki)) prevent(:download_wiki_code) end rule { builds_disabled | repository_disabled }.policy do - prevent(*create_read_update_admin(:build)) - prevent(*(create_read_update_admin(:pipeline) - [:read_pipeline])) - prevent(*create_read_update_admin(:pipeline_schedule)) - prevent(*create_read_update_admin(:environment)) - prevent(*create_read_update_admin(:deployment)) + prevent(*create_update_admin_destroy(:pipeline)) + prevent(*create_read_update_admin_destroy(:build)) + prevent(*create_read_update_admin_destroy(:pipeline_schedule)) + prevent(*create_read_update_admin_destroy(:environment)) + prevent(*create_read_update_admin_destroy(:deployment)) end rule { repository_disabled }.policy do @@ -272,7 +301,7 @@ class ProjectPolicy < BasePolicy end rule { container_registry_disabled }.policy do - prevent(*create_read_update_admin(:container_image)) + prevent(*create_read_update_admin_destroy(:container_image)) end rule { anonymous & ~public_project }.prevent_all @@ -314,13 +343,6 @@ class ProjectPolicy < BasePolicy enable :read_pipeline_schedule end - rule { issues_disabled }.policy do - prevent :create_issue - prevent :update_issue - prevent :admin_issue - prevent :read_issue - end - # These rules are included to allow maintainers of projects to push to certain # to run pipelines for the branches they have access to. rule { can?(:public_access) & has_merge_requests_allowing_pushes }.policy do diff --git a/app/policies/project_policy/class_methods.rb b/app/policies/project_policy/class_methods.rb new file mode 100644 index 00000000000..60e5aba00ba --- /dev/null +++ b/app/policies/project_policy/class_methods.rb @@ -0,0 +1,19 @@ +class ProjectPolicy + module ClassMethods + def create_read_update_admin_destroy(name) + [ + :"read_#{name}", + *create_update_admin_destroy(name) + ] + end + + def create_update_admin_destroy(name) + [ + :"create_#{name}", + :"update_#{name}", + :"admin_#{name}", + :"destroy_#{name}" + ] + end + end +end diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index 9f3f2637183..4b4132af2d0 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -3,6 +3,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated include GitlabRoutingHelper include MarkupHelper include TreeHelper + include ChecksCollaboration include Gitlab::Utils::StrongMemoize presents :merge_request @@ -152,11 +153,11 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end def can_revert_on_current_merge_request? - user_can_collaborate_with_project? && cached_can_be_reverted? + can_collaborate_with_project?(project) && cached_can_be_reverted? end def can_cherry_pick_on_current_merge_request? - user_can_collaborate_with_project? && can_be_cherry_picked? + can_collaborate_with_project?(project) && can_be_cherry_picked? end def can_push_to_source_branch? @@ -195,12 +196,6 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end.sort.to_sentence end - def user_can_collaborate_with_project? - can?(current_user, :push_code, project) || - (current_user && current_user.already_forked?(project)) || - can_push_to_source_branch? - end - def user_can_fork_project? can?(current_user, :fork_project, project) end diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb index b5e2334b6e3..840fdbcbf14 100644 --- a/app/serializers/issue_entity.rb +++ b/app/serializers/issue_entity.rb @@ -29,6 +29,10 @@ class IssueEntity < IssuableEntity expose :can_update do |issue| can?(request.current_user, :update_issue, issue) end + + expose :can_award_emoji do |issue| + can?(request.current_user, :award_emoji, issue) + end end expose :create_note_path do |issue| diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb index c964aa9c99b..06d603b277e 100644 --- a/app/serializers/note_entity.rb +++ b/app/serializers/note_entity.rb @@ -15,7 +15,11 @@ class NoteEntity < API::Entities::Note expose :current_user do expose :can_edit do |note| - Ability.can_edit_note?(request.current_user, note) + Ability.allowed?(request.current_user, :admin_note, note) + end + + expose :can_award_emoji do |note| + Ability.allowed?(request.current_user, :award_emoji, note) end end diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index 15ab2d54404..84944e95542 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -13,7 +13,7 @@ module Clusters rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") rescue ActiveRecord::RecordInvalid => e - provider.make_errored!("Failed to configure GKE Cluster: #{e.message}") + provider.make_errored!("Failed to configure Google Kubernetes Engine Cluster: #{e.message}") end private diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index c57a2445341..fe1ac70781e 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -71,8 +71,8 @@ module MergeRequests params.delete(:source_project_id) params.delete(:target_project_id) - unless can?(current_user, :read_project, @source_project) && - can?(current_user, :read_project, @project) + unless can?(current_user, :create_merge_request_from, @source_project) && + can?(current_user, :create_merge_request_in, @project) raise Gitlab::Access::AccessDeniedError end diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml index 864e64b5fa9..48331c40bca 100644 --- a/app/views/admin/application_settings/_signin.html.haml +++ b/app/views/admin/application_settings/_signin.html.haml @@ -24,6 +24,7 @@ - if omniauth_enabled? && button_based_providers.any? .form-group = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2' + = hidden_field_tag 'application_setting[enabled_oauth_sign_in_sources][]' .col-sm-10 .btn-group{ data: { toggle: 'buttons' } } - oauth_providers_checkboxes.each do |source| diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml index cbc779548f6..a75dd90fe6b 100644 --- a/app/views/admin/application_settings/_visibility_and_access.html.haml +++ b/app/views/admin/application_settings/_visibility_and_access.html.haml @@ -32,6 +32,7 @@ .form-group = f.label :import_sources, class: 'control-label col-sm-2' .col-sm-10 + = hidden_field_tag 'application_setting[import_sources][]' - import_sources_checkboxes('import-sources-help').each do |source| .checkbox= source %span.help-block#import-sources-help diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 05c41082882..bbf0e0fb95c 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -126,6 +126,7 @@ GitLab %span.pull-right = Gitlab::VERSION + = "(#{Gitlab::REVISION})" %p GitLab Shell %span.pull-right diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index c47b8a88f56..aeba9788fda 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -101,7 +101,7 @@ - if @project.archived? %li %span.light archived: - %strong repository is read-only + %strong project is read-only %li %span.light access: diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index 5f07d2720c2..4b3c52af16a 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -3,13 +3,13 @@ .awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } } - awards_sort(grouped_emojis).each do |emoji, awards| %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", - class: [(award_state_class(awards, current_user)), (award_user_authored_class(emoji) if user_authored)], + class: [(award_state_class(awardable, awards, current_user)), (award_user_authored_class(emoji) if user_authored)], data: { placement: "bottom", title: award_user_list(awards, current_user) } } = emoji_icon(emoji) %span.award-control-text.js-counter = awards.count - - if current_user + - if can?(current_user, :award_emoji, awardable) .award-menu-holder.js-award-holder %button.btn.award-control.has-tooltip.js-add-award{ type: 'button', 'aria-label': 'Add reaction', diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index eb32f393310..6f53f5ac1ae 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -19,8 +19,8 @@ %li.dropdown-bold-header GitLab - if @project&.persisted? - - create_project_issue = can?(current_user, :create_issue, @project) - - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) + - create_project_issue = show_new_issue_link?(@project) + - merge_project = merge_request_source_project_for_project(@project) - create_project_snippet = can?(current_user, :create_project_snippet, @project) - if create_project_issue || merge_project || create_project_snippet %li.dropdown-bold-header This project diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 93f674b9d3c..196db08cebd 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -13,7 +13,7 @@ .nav-icon-container = sprite_icon('project') %span.nav-item-name - Overview + Project %ul.sidebar-sub-level-items = nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 1bd10018b40..d1eae05c46c 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -20,7 +20,7 @@ - else %p Download the Google Authenticator application from App Store or Google Play Store and scan this code. - More information is available in the #{link_to('documentation', help_page_path('profile/two_factor_authentication'))}. + More information is available in the #{link_to('documentation', help_page_path('user/profile/account/two_factor_authentication'))}. .row.append-bottom-10 .col-md-4 = raw @qr_code diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml index 6a1035d2dc7..f6d396c8127 100644 --- a/app/views/projects/_last_push.html.haml +++ b/app/views/projects/_last_push.html.haml @@ -13,6 +13,7 @@ #{time_ago_with_tooltip(event.created_at)} - .flex-right - = link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm qa-create-merge-request" do - #{ _('Create merge request') } + - if can?(current_user, :create_merge_request_in, event.project.default_merge_request_target) + .flex-right + = link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm qa-create-merge-request" do + #{ _('Create merge request') } diff --git a/app/views/projects/_visibility_select.html.haml b/app/views/projects/_visibility_select.html.haml deleted file mode 100644 index 4026b9e3c46..00000000000 --- a/app/views/projects/_visibility_select.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- if can_change_visibility_level?(@project, current_user) - .select-wrapper - = form.select(model_method, visibility_select_options(@project, selected_level), {}, class: 'form-control visibility-select select-control') - = icon('chevron-down') -- else - .info.js-locked{ data: { help_block: visibility_level_description(@project.visibility_level, @project) } } - = visibility_level_icon(@project.visibility_level) - %strong - = visibility_level_label(@project.visibility_level) diff --git a/app/views/projects/blob/_viewer.html.haml b/app/views/projects/blob/_viewer.html.haml index 3124443b4e4..9c760c81527 100644 --- a/app/views/projects/blob/_viewer.html.haml +++ b/app/views/projects/blob/_viewer.html.haml @@ -2,6 +2,7 @@ - render_error = viewer.render_error - rich_type = viewer.type == :rich ? viewer.partial_name : nil - load_async = local_assigns.fetch(:load_async, viewer.load_async? && render_error.nil?) +- external_embed = local_assigns.fetch(:external_embed, false) - viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_async .blob-viewer{ data: { type: viewer.type, rich_type: rich_type, url: viewer_url }, class: ('hidden' if hidden) } @@ -9,6 +10,8 @@ = render 'projects/blob/render_error', viewer: viewer - elsif load_async = render viewer.loading_partial_path, viewer: viewer + - elsif external_embed + = render 'projects/blob/viewers/highlight_embed', blob: viewer.blob - else - viewer.prepare! diff --git a/app/views/projects/blob/viewers/_highlight_embed.html.haml b/app/views/projects/blob/viewers/_highlight_embed.html.haml new file mode 100644 index 00000000000..9bd4ef6ad0b --- /dev/null +++ b/app/views/projects/blob/viewers/_highlight_embed.html.haml @@ -0,0 +1,7 @@ +.file-content.code.js-syntax-highlight + .line-numbers + - if blob.data.present? + - blob.data.each_line.each_with_index do |_, index| + %span.diff-line-num= index + 1 + .blob-content{ data: { blob_id: blob.id } } + = highlight(blob.path, blob.data, repository: nil, plain: blob.no_highlighting?) diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 883dfb3e6c8..71176acd12d 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -4,7 +4,7 @@ - diverging_commit_counts = @repository.diverging_commit_counts(branch) - number_commits_behind = diverging_commit_counts[:behind] - number_commits_ahead = diverging_commit_counts[:ahead] -- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) +- merge_project = merge_request_source_project_for_project(@project) %li{ class: "branch-item js-branch-#{branch.name}" } .branch-info .branch-title @@ -61,7 +61,7 @@ title: s_('Branches|The default branch cannot be deleted') } = icon("trash-o") - elsif protected_branch?(@project, branch) - - if can?(current_user, :delete_protected_branch, @project) + - if can?(current_user, :push_to_delete_protected_branch, @project) %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip", title: s_('Branches|Delete protected branch'), data: { toggle: "modal", diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 18e948ce35a..2e86a7d36d7 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -1,13 +1,17 @@ -- if current_user +- can_create_issue = show_new_issue_link?(@project) +- can_create_project_snippet = can?(current_user, :create_project_snippet, @project) +- can_push_code = can?(current_user, :push_code, @project) +- create_mr_from_new_fork = can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project) +- merge_project = merge_request_source_project_for_project(@project) + +- show_menu = can_create_issue || can_create_project_snippet || can_push_code || create_mr_from_new_fork || merge_project + +- if show_menu .project-action-button.dropdown.inline %a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...') } = icon('plus') = icon("caret-down") %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown - - can_create_issue = can?(current_user, :create_issue, @project) - - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - - can_create_project_snippet = can?(current_user, :create_project_snippet, @project) - - if can_create_issue || merge_project || can_create_project_snippet %li.dropdown-header= _('This project') @@ -20,17 +24,17 @@ - if can_create_project_snippet %li= link_to _('New snippet'), new_project_snippet_path(@project) - - if can?(current_user, :push_code, @project) + - if can_push_code %li.dropdown-header= _('This repository') - - if can?(current_user, :push_code, @project) + - if can_push_code %li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master') - unless @project.empty_repo? %li= link_to _('New branch'), new_project_branch_path(@project) %li= link_to _('New tag'), new_project_tag_path(@project) - - elsif current_user && current_user.already_forked?(@project) + - elsif can_collaborate_with_project?(@project) %li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master') - - elsif can?(current_user, :fork_project, @project) + - elsif create_mr_from_new_fork - continue_params = { to: project_new_blob_path(@project, @project.default_branch || 'master'), notice: edit_in_new_fork_notice, notice_now: edit_in_new_fork_notice_now } diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/projects/clusters/_empty_state.html.haml index 112dde66ff7..5f49d03b1bb 100644 --- a/app/views/projects/clusters/_empty_state.html.haml +++ b/app/views/projects/clusters/_empty_state.html.haml @@ -7,5 +7,6 @@ - link_to_help_page = link_to(_('Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') %p= s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page} - .text-center - = link_to s_('ClusterIntegration|Add Kubernetes cluster'), new_project_cluster_path(@project), class: 'btn btn-success' + - if can?(current_user, :create_cluster, @project) + .text-center + = link_to s_('ClusterIntegration|Add Kubernetes cluster'), new_project_cluster_path(@project), class: 'btn btn-success' diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml index ebb7d247125..e004966bdcc 100644 --- a/app/views/projects/clusters/new.html.haml +++ b/app/views/projects/clusters/new.html.haml @@ -8,6 +8,6 @@ %h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up Kubernetes cluster integration') %p= s_('ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab') - = link_to s_('ClusterIntegration|Create on GKE'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20' + = link_to s_('ClusterIntegration|Create on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20' %p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster') = link_to s_('ClusterIntegration|Add an existing Kubernetes cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20' diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 74c5317428c..213c4c90a0e 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,3 +1,5 @@ +- can_collaborate = can_collaborate_with_project?(@project) + .page-content-header.js-commit-box{ 'data-commit-path' => branches_project_commit_path(@project, @commit.id) } .header-main-content = render partial: 'signature', object: @commit.signature @@ -32,12 +34,13 @@ %li.visible-xs-block.visible-sm-block = link_to project_tree_path(@project, @commit) do #{ _('Browse Files') } - - unless @commit.has_been_reverted?(current_user) + - if can_collaborate && !@commit.has_been_reverted?(current_user) %li.clearfix = revert_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false) - %li.clearfix - = cherry_pick_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false) - - if can_collaborate_with_project? + - if can_collaborate + %li.clearfix + = cherry_pick_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false) + - if can?(current_user, :push_code, @project) %li.clearfix = link_to s_("CreateTag|Tag"), new_project_tag_path(@project, ref: @commit) %li.divider diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index abb292f8f27..541ae905246 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -17,6 +17,6 @@ .limited-width-notes = render "shared/notes/notes_with_form", :autocomplete => true - - if can_collaborate_with_project? + - if can_collaborate_with_project?(@project) - %w(revert cherry-pick).each do |type| = render "projects/commit/change", type: type, commit: @commit, title: @commit.title diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 99eeb9551e3..0994498c6be 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -114,17 +114,18 @@ Archive project - if @project.archived? %p - Unarchiving the project will mark its repository as active. The project can be committed to. + Unarchiving the project will restore people's ability to make changes to it. + The repository can be committed to, and issues, comments and other entities can be created. %strong Once active this project shows up in the search and on the dashboard. = link_to 'Unarchive project', unarchive_project_path(@project), - data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." }, + data: { confirm: "Are you sure that you want to unarchive this project?" }, method: :post, class: "btn btn-success" - else %p - Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches. - %strong Archived projects cannot be committed to! + Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. + %strong The repository cannot be committed to, and no issues, comments or other entities can be created. = link_to 'Archive project', archive_project_path(@project), - data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." }, + data: { confirm: "Are you sure that you want to archive this project?" }, method: :post, class: "btn btn-warning" .sub-section.rename-respository %h4.warning-title diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml index 0d39edb7bfd..dd1a836fa20 100644 --- a/app/views/projects/issues/_nav_btns.html.haml +++ b/app/views/projects/issues/_nav_btns.html.haml @@ -2,9 +2,10 @@ = icon('rss') - if @can_bulk_update = button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle" -= link_to "New issue", new_project_issue_path(@project, - issue: { assignee_id: finder.assignee.try(:id), - milestone_id: finder.milestones.first.try(:id) }), - class: "btn btn-new", - title: "New issue", - id: "new_issue_link" +- if show_new_issue_link?(@project) + = link_to "New issue", new_project_issue_path(@project, + issue: { assignee_id: finder.assignee.try(:id), + milestone_id: finder.milestones.first.try(:id) }), + class: "btn btn-new", + title: "New issue", + id: "new_issue_link" diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index 36e24037214..4b8bf578b28 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -1,8 +1,8 @@ -- can_create_merge_request = can?(current_user, :create_merge_request, @project) -- data_action = can_create_merge_request ? 'create-mr' : 'create-branch' -- value = can_create_merge_request ? 'Create merge request' : 'Create branch' - - if can?(current_user, :push_code, @project) + - can_create_merge_request = can?(current_user, :create_merge_request_in, @project) + - data_action = can_create_merge_request ? 'create-mr' : 'create-branch' + - value = can_create_merge_request ? 'Create merge request' : 'Create branch' + - can_create_path = can_create_branch_project_issue_path(@project, @issue) - create_mr_path = create_merge_request_project_issue_path(@project, @issue, branch_name: @issue.to_branch_name, ref: @project.default_branch) - create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid) diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index ec7e87219f5..f1fc1c2316d 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -7,6 +7,7 @@ - can_update_issue = can?(current_user, :update_issue, @issue) - can_report_spam = @issue.submittable_as_spam_by?(current_user) +- can_create_issue = show_new_issue_link?(@project) .detail-page-header .detail-page-header-body @@ -42,16 +43,18 @@ %li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' - if can_report_spam %li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam' - - if can_update_issue || can_report_spam - %li.divider - %li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link' + - if can_create_issue + - if can_update_issue || can_report_spam + %li.divider + %li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link' = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue - if can_report_spam = link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam' - = link_to new_project_issue_path(@project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do - New issue + - if can_create_issue + = link_to new_project_issue_path(@project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do + New issue .issue-details.issuable-details .detail-page-description.content-block diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index b2c0d9e1cfa..623380c9c61 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -1,6 +1,6 @@ - @no_container = true - @can_bulk_update = can?(current_user, :admin_merge_request, @project) -- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) +- merge_project = merge_request_source_project_for_project(@project) - new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project - page_title "Merge Requests" diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml index 5ea653ccad5..b4fe1cabdfd 100644 --- a/app/views/projects/notes/_actions.html.haml +++ b/app/views/projects/notes/_actions.html.haml @@ -36,7 +36,7 @@ %template{ 'v-else' => '' } = render 'shared/icons/icon_resolve_discussion.svg' -- if current_user +- if can?(current_user, :award_emoji, note) - if note.emoji_awardable? - user_authored = note.user_authored?(current_user) .note-actions-item diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 94331a16abd..e28accd5b43 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -24,7 +24,7 @@ .text-warning.center.prepend-top-20 %p = icon("exclamation-triangle fw") - #{ _('Archived project! Repository is read-only') } + #{ _('Archived project! Repository and other project resources are read-only') } - view_path = @project.default_view diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 3d5f92f9aaa..98b4d6339da 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -31,6 +31,6 @@ = link_to edit_project_tag_release_path(@project, tag.name), class: 'btn has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do = icon("pencil") - - if can?(current_user, :admin_project, @project) - = link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip prepend-left-10 #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do - = icon("trash-o") + - if can?(current_user, :admin_project, @project) + = link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip prepend-left-10 #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do + = icon("trash-o") diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index dfe2c37ed8e..7a3469cdd26 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -28,7 +28,7 @@ = icon('history') .btn-container.controls-item = render 'projects/buttons/download', project: @project, ref: @tag.name - - if can?(current_user, :admin_project, @project) + - if can?(current_user, :push_code, @project) && can?(current_user, :admin_project, @project) .btn-container.controls-item-full = link_to project_tag_path(@project, @tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, @tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: @tag.name } } do %i.fa.fa-trash-o diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 5ef5e9c09a2..8587d3b0c0d 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -1,3 +1,6 @@ +- can_collaborate = can_collaborate_with_project?(@project) +- can_create_mr_from_fork = can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project) + .tree-ref-container .tree-ref-holder = render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true @@ -15,7 +18,7 @@ %li = link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path)) - - if current_user + - if can_collaborate || can_create_mr_from_fork %li %a.btn.add-to-tree{ addtotree_toggle_attributes } = sprite_icon('plus', size: 16, css_class: 'pull-left') @@ -35,7 +38,7 @@ %li = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do #{ _('New directory') } - - elsif can?(current_user, :fork_project, @project) + - elsif can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project) %li - continue_params = { to: project_new_blob_path(@project, @id), notice: edit_in_new_fork_notice, @@ -61,23 +64,25 @@ = link_to fork_path, method: :post do #{ _('New directory') } - %li.divider - %li.dropdown-header - #{ _('This repository') } - %li - = link_to new_project_branch_path(@project) do - #{ _('New branch') } - %li - = link_to new_project_tag_path(@project) do - #{ _('New tag') } + - if can?(current_user, :push_code, @project) + %li.divider + %li.dropdown-header + #{ _('This repository') } + %li + = link_to new_project_branch_path(@project) do + #{ _('New branch') } + %li + = link_to new_project_tag_path(@project) do + #{ _('New tag') } .tree-controls = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn' = render 'projects/find_file_link' - = succeed " " do - = link_to ide_edit_path(@project, @id, ""), class: 'btn btn-default' do - = _('Web IDE') + - if can_collaborate + = succeed " " do + = link_to ide_edit_path(@project, @id, ""), class: 'btn btn-default' do + = _('Web IDE') = render 'projects/buttons/download', project: @project, ref: @ref diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml index 934d65e8b42..e9ac192f5f7 100644 --- a/app/views/shared/_auto_devops_callout.html.haml +++ b/app/views/shared/_auto_devops_callout.html.haml @@ -1,14 +1,14 @@ -.js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } } +.js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20.prepend-top-10{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } } .banner-graphic = custom_icon('icon_autodevops') - .prepend-top-10.prepend-left-10.append-bottom-10 - %h5= s_('AutoDevOps|Auto DevOps (Beta)') + .banner-body.prepend-left-10.append-bottom-10 + %h5.banner-title= s_('AutoDevOps|Auto DevOps (Beta)') %p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') %p - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer') = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link } - .prepend-top-10 + .banner-buttons = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout' %button.btn-transparent.banner-close.close.js-close-callout{ type: 'button', diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index 56403907844..836df57a3a2 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -47,20 +47,20 @@ class: 'text-danger' .pull-right.hidden-xs.hidden-sm - - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) - %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'), - disabled: true, - type: 'button', - data: { url: promote_project_label_path(label.project, label), - label_title: label.title, - label_color: label.color, - label_text_color: label.text_color, - group_name: label.project.group.name, - target: '#promote-label-modal', - container: 'body', - toggle: 'modal' } } - = sprite_icon('level-up') - if can?(current_user, :admin_label, label) + - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) + %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'), + disabled: true, + type: 'button', + data: { url: promote_project_label_path(label.project, label), + label_title: label.title, + label_color: label.color, + label_text_color: label.text_color, + group_name: label.project.group.name, + target: '#promote-label-modal', + container: 'body', + toggle: 'modal' } } + = sprite_icon('level-up') = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do %span.sr-only Edit = sprite_icon('pencil') diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml index 5868c52566d..fc634856061 100644 --- a/app/views/shared/members/_group.html.haml +++ b/app/views/shared/members/_group.html.haml @@ -8,7 +8,7 @@ %strong = link_to group.full_name, group_path(group) .cgray - Joined #{time_ago_with_tooltip(group.created_at)} + Given access #{time_ago_with_tooltip(group_link.created_at)} - if group_link.expires? · %span{ class: ('text-warning' if group_link.expires_soon?) } diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index ba57d922c6d..1c139827acf 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -29,7 +29,7 @@ Requested = time_ago_with_tooltip(member.requested_at) - else - Joined #{time_ago_with_tooltip(member.created_at)} + Given access #{time_ago_with_tooltip(member.created_at)} - if member.expires? · %span{ class: "#{"text-warning" if member.expires_soon?} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) } diff --git a/app/views/shared/milestones/_deprecation_message.html.haml b/app/views/shared/milestones/_deprecation_message.html.haml new file mode 100644 index 00000000000..4a8f90937ea --- /dev/null +++ b/app/views/shared/milestones/_deprecation_message.html.haml @@ -0,0 +1,14 @@ +.banner-callout.compact.milestone-deprecation-message.js-milestone-deprecation-message.prepend-top-20 + .banner-graphic= image_tag 'illustrations/milestone_removing-page.svg' + .banner-body.prepend-left-10.append-right-10 + %h5.banner-title.prepend-top-0= _('This page will be removed in a future release.') + %p.milestone-banner-text= _('Use group milestones to manage issues from multiple projects in the same milestone.') + = button_tag _('Promote these project milestones into a group milestone.'), class: 'btn btn-link js-popover-link text-align-left milestone-banner-link' + .milestone-banner-buttons.prepend-top-20= link_to _('Learn more'), help_page_url('user/project/milestones/index', anchor: 'promoting-project-milestones-to-group-milestones'), class: 'btn btn-default', target: '_blank' + + %template.js-milestone-deprecation-message-template + .milestone-popover-body + %ol.milestone-popover-instructions-list.append-bottom-0 + %li= _('Click any <strong>project name</strong> in the project list below to navigate to the project milestone.').html_safe + %li= _('Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone.').html_safe + .milestone-popover-footer= link_to _('Learn more'), help_page_url('user/project/milestones/index', anchor: 'promoting-project-milestones-to-group-milestones'), class: 'btn btn-link prepend-left-0', target: '_blank' diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index a942ebc328b..ee134480705 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -72,7 +72,7 @@ .title.hide-collapsed Issues %span.badge= milestone.issues_visible_to_user(current_user).count - - if project && can?(current_user, :create_issue, project) + - if show_new_issue_link?(project) = link_to new_project_issue_path(project, issue: { milestone_id: milestone.id }), class: "pull-right", title: "New Issue" do New issue .value.hide-collapsed.bold diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index f302299eb24..797ff034bb2 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -1,7 +1,8 @@ -- page_title @milestone.title +- page_title milestone.title - @breadcrumb_link = dashboard_milestone_path(milestone.safe_title, title: milestone.title) - group = local_assigns[:group] +- is_dynamic_milestone = milestone.legacy_group_milestone? || milestone.dashboard_milestone? .detail-page-header %a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" } @@ -31,21 +32,23 @@ - else = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" += render 'shared/milestones/deprecation_message' if is_dynamic_milestone + .detail-page-description.milestone-detail %h2.title = markdown_field(milestone, :title) - - if @milestone.group_milestone? && @milestone.description.present? + - if milestone.group_milestone? && milestone.description.present? %div .description .wiki - = markdown_field(@milestone, :description) + = markdown_field(milestone, :description) - if milestone.complete?(current_user) && milestone.active? .alert.alert-success.prepend-top-default - close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.' %span All issues for this milestone are closed. #{close_msg} -- if @milestone.legacy_group_milestone? || @milestone.dashboard_milestone? +- if is_dynamic_milestone .table-holder %table.table %thead @@ -68,7 +71,7 @@ Open %td = ms.expires_at -- elsif @milestone.group_milestone? +- elsif milestone.group_milestone? %br View = link_to 'Issues', issues_group_path(@group, milestone_title: milestone.title) diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml index 725bf916592..71c0d740bc8 100644 --- a/app/views/shared/notes/_form.html.haml +++ b/app/views/shared/notes/_form.html.haml @@ -24,20 +24,21 @@ -# DiffNote = f.hidden_field :position - = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do - = render 'projects/zen', f: f, - attr: :note, - classes: 'note-textarea js-note-text', - placeholder: "Write a comment or drag your files here...", - supports_quick_actions: supports_quick_actions, - supports_autocomplete: supports_autocomplete - = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions - .error-alert - - .note-form-actions.clearfix - = render partial: 'shared/notes/comment_button' - - = yield(:note_actions) - - %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } } - Discard draft + .discussion-form-container + = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do + = render 'projects/zen', f: f, + attr: :note, + classes: 'note-textarea js-note-text', + placeholder: "Write a comment or drag your files here...", + supports_quick_actions: supports_quick_actions, + supports_autocomplete: supports_autocomplete + = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions + .error-alert + + .note-form-actions.clearfix + = render partial: 'shared/notes/comment_button' + + = yield(:note_actions) + + %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } } + Discard draft diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml index bf359774ead..893a7f26ebd 100644 --- a/app/views/shared/notes/_note.html.haml +++ b/app/views/shared/notes/_note.html.haml @@ -2,7 +2,7 @@ - return if note.cross_reference_not_visible_for?(current_user) - show_image_comment_badge = local_assigns.fetch(:show_image_comment_badge, false) -- note_editable = note_editable?(note) +- note_editable = can?(current_user, :admin_note, note) - note_counter = local_assigns.fetch(:note_counter, 0) %li.timeline-entry{ id: dom_id(note), diff --git a/app/views/shared/snippets/_embed.html.haml b/app/views/shared/snippets/_embed.html.haml new file mode 100644 index 00000000000..2d93e51a2d9 --- /dev/null +++ b/app/views/shared/snippets/_embed.html.haml @@ -0,0 +1,24 @@ +- blob = @snippet.blob +.gitlab-embed-snippets + .js-file-title.file-title-flex-parent + .file-header-content + = external_snippet_icon('doc_text') + + %strong.file-title-name + %a.gitlab-embedded-snippets-title{ href: url_for(only_path: false, overwrite_params: nil) } + = blob.name + + %small + = number_to_human_size(blob.raw_size) + %a.gitlab-logo{ href: url_for(only_path: false, overwrite_params: nil), title: 'view on gitlab' } + on + %span.logo-text + GitLab + + .file-actions.hidden-xs + .btn-group{ role: "group" }< + = embedded_snippet_raw_button + + = embedded_snippet_download_button + %article.file-holder.snippet-file-content + = render 'projects/blob/viewer', viewer: @snippet.blob.simple_viewer, load_async: false, external_embed: true diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 12df79a28c7..836230ae8ee 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -19,11 +19,32 @@ %h2.snippet-title.prepend-top-0.append-bottom-0 = markdown_field(@snippet, :title) - - if @snippet.updated_at != @snippet.created_at - = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago', exclude_author: true) - if @snippet.description.present? .description .wiki = markdown_field(@snippet, :description) %textarea.hidden.js-task-list-field = @snippet.description + + - if @snippet.updated_at != @snippet.created_at + = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago', exclude_author: true) + + - if public_snippet? + .embed-snippet + .input-group + .input-group-btn + %button.btn.embed-toggle{ 'data-toggle': 'dropdown', type: 'button' } + %span.js-embed-action= _("Embed") + = sprite_icon('angle-down', size: 12) + %ul.dropdown-menu.dropdown-menu-selectable.embed-toggle-list + %li + %button.js-embed-btn.btn.btn-transparent.is-active{ type: 'button' } + %strong.embed-toggle-list-item= _("Embed") + %li + %button.js-share-btn.btn.btn-transparent{ type: 'button' } + %strong.embed-toggle-list-item= _("Share") + %input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed } + .input-group-btn + %button.js-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '#snippet-url-area' } + = sprite_icon('duplicate', size: 16) + .clearfix diff --git a/app/views/shared/snippets/show.js.haml b/app/views/shared/snippets/show.js.haml new file mode 100644 index 00000000000..a9af732bbb5 --- /dev/null +++ b/app/views/shared/snippets/show.js.haml @@ -0,0 +1,2 @@ +document.write('#{escape_javascript(stylesheet_link_tag "#{stylesheet_url 'snippets'}")}'); +document.write('#{escape_javascript(render 'shared/snippets/embed')}'); |