diff options
author | Phil Hughes <me@iamphill.com> | 2018-04-18 14:19:11 +0100 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2018-04-18 14:19:11 +0100 |
commit | 64be67f322f0aa4acee8df5062631a6ee867f730 (patch) | |
tree | 3b733e113bee9b6b8c1bbc29c7dba00900bb4097 /app | |
parent | 48fb30f730f0bf8c02c21d992cb83fdf35a00e91 (diff) | |
parent | 9c2f6e04cf5d2eb89d0ec68e8ace87b3804e9ae5 (diff) | |
download | gitlab-ce-64be67f322f0aa4acee8df5062631a6ee867f730.tar.gz |
Merge branch 'master' into ide-file-finder
Diffstat (limited to 'app')
131 files changed, 1813 insertions, 968 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/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue index 2cbd982af19..45321df191c 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue @@ -1,41 +1,27 @@ <script> - import { mapState } from 'vuex'; - import { sprintf, __ } from '~/locale'; - import * as consts from '../../stores/modules/commit/constants'; - import RadioGroup from './radio_group.vue'; +import { mapState } from 'vuex'; +import { sprintf, __ } from '~/locale'; +import * as consts from '../../stores/modules/commit/constants'; +import RadioGroup from './radio_group.vue'; - export default { - components: { - RadioGroup, +export default { + components: { + RadioGroup, + }, + computed: { + ...mapState(['currentBranchId']), + commitToCurrentBranchText() { + return sprintf( + __('Commit to %{branchName} branch'), + { branchName: `<strong class="monospace">${this.currentBranchId}</strong>` }, + false, + ); }, - computed: { - ...mapState([ - 'currentBranchId', - ]), - newMergeRequestHelpText() { - return sprintf( - __('Creates a new branch from %{branchName} and re-directs to create a new merge request'), - { branchName: this.currentBranchId }, - ); - }, - commitToCurrentBranchText() { - return sprintf( - __('Commit to %{branchName} branch'), - { branchName: `<strong>${this.currentBranchId}</strong>` }, - false, - ); - }, - commitToNewBranchText() { - return sprintf( - __('Creates a new branch from %{branchName}'), - { branchName: this.currentBranchId }, - ); - }, - }, - commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH, - commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH, - commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR, - }; + }, + commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH, + commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH, + commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR, +}; </script> <template> @@ -53,13 +39,11 @@ :value="$options.commitToNewBranch" :label="__('Create a new branch')" :show-input="true" - :help-text="commitToNewBranchText" /> <radio-group :value="$options.commitToNewBranchMR" :label="__('Create a new branch and merge request')" :show-input="true" - :help-text="newMergeRequestHelpText" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue new file mode 100644 index 00000000000..dcd934f76b7 --- /dev/null +++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue @@ -0,0 +1,130 @@ +<script> +import { __, sprintf } from '../../../locale'; +import Icon from '../../../vue_shared/components/icon.vue'; +import popover from '../../../vue_shared/directives/popover'; +import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants'; + +export default { + directives: { + popover, + }, + components: { + Icon, + }, + props: { + text: { + type: String, + required: true, + }, + }, + data() { + return { + scrollTop: 0, + isFocused: false, + }; + }, + computed: { + allLines() { + return this.text.split('\n').map((line, i) => ({ + text: line.substr(0, this.getLineLength(i)) || ' ', + highlightedText: line.substr(this.getLineLength(i)), + })); + }, + }, + methods: { + handleScroll() { + if (this.$refs.textarea) { + this.$nextTick(() => { + this.scrollTop = this.$refs.textarea.scrollTop; + }); + } + }, + getLineLength(i) { + return i === 0 ? MAX_TITLE_LENGTH : MAX_BODY_LENGTH; + }, + onInput(e) { + this.$emit('input', e.target.value); + }, + updateIsFocused(isFocused) { + this.isFocused = isFocused; + }, + }, + popoverOptions: { + trigger: 'hover', + placement: 'top', + content: sprintf( + __(` + The character highligher helps you keep the subject line to %{titleLength} characters + and wrap the body at %{bodyLength} so they are readable in git. + `), + { titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH }, + ), + }, +}; +</script> + +<template> + <fieldset class="common-note-form ide-commit-message-field"> + <div + class="md-area" + :class="{ + 'is-focused': isFocused + }" + > + <div + v-once + class="md-header" + > + <ul class="nav-links"> + <li> + {{ __('Commit Message') }} + <span + v-popover="$options.popoverOptions" + class="help-block prepend-left-10" + > + <icon + name="question" + /> + </span> + </li> + </ul> + </div> + <div class="ide-commit-message-textarea-container"> + <div class="ide-commit-message-highlights-container"> + <div + class="note-textarea highlights monospace" + :style="{ + transform: `translate3d(0, ${-scrollTop}px, 0)` + }" + > + <div + v-for="(line, index) in allLines" + :key="index" + > + <span + v-text="line.text" + > + </span><mark + v-show="line.highlightedText" + v-text="line.highlightedText" + > + </mark> + </div> + </div> + </div> + <textarea + class="note-textarea ide-commit-message-textarea" + name="commit-message" + :placeholder="__('Write a commit message...')" + :value="text" + @scroll="handleScroll" + @input="onInput" + @focus="updateIsFocused(true)" + @blur="updateIsFocused(false)" + ref="textarea" + > + </textarea> + </div> + </div> + </fieldset> +</template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue index 4310d762c78..b660a2961cb 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue @@ -1,52 +1,40 @@ <script> - import { mapActions, mapState, mapGetters } from 'vuex'; - import tooltip from '~/vue_shared/directives/tooltip'; +import { mapActions, mapState, mapGetters } from 'vuex'; +import tooltip from '~/vue_shared/directives/tooltip'; - export default { - directives: { - tooltip, +export default { + directives: { + tooltip, + }, + props: { + value: { + type: String, + required: true, }, - props: { - value: { - type: String, - required: true, - }, - label: { - type: String, - required: false, - default: null, - }, - checked: { - type: Boolean, - required: false, - default: false, - }, - showInput: { - type: Boolean, - required: false, - default: false, - }, - helpText: { - type: String, - required: false, - default: null, - }, + label: { + type: String, + required: false, + default: null, }, - computed: { - ...mapState('commit', [ - 'commitAction', - ]), - ...mapGetters('commit', [ - 'newBranchName', - ]), + checked: { + type: Boolean, + required: false, + default: false, }, - methods: { - ...mapActions('commit', [ - 'updateCommitAction', - 'updateBranchName', - ]), + showInput: { + type: Boolean, + required: false, + default: false, }, - }; + }, + computed: { + ...mapState('commit', ['commitAction']), + ...mapGetters('commit', ['newBranchName']), + }, + methods: { + ...mapActions('commit', ['updateCommitAction', 'updateBranchName']), + }, +}; </script> <template> @@ -65,18 +53,6 @@ {{ label }} </template> <slot v-else></slot> - <span - v-if="helpText" - v-tooltip - class="help-block inline" - :title="helpText" - > - <i - class="fa fa-question-circle" - aria-hidden="true" - > - </i> - </span> </span> </label> <div @@ -85,7 +61,7 @@ > <input type="text" - class="form-control" + class="form-control monospace" :placeholder="newBranchName" @input="updateBranchName($event.target.value)" /> diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index d885ed5e301..14673754503 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -5,6 +5,7 @@ import icon from '~/vue_shared/components/icon.vue'; import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue'; import commitFilesList from './commit_sidebar/list.vue'; +import CommitMessageField from './commit_sidebar/message_field.vue'; import * as consts from '../stores/modules/commit/constants'; import Actions from './commit_sidebar/actions.vue'; @@ -15,6 +16,7 @@ export default { commitFilesList, Actions, LoadingButton, + CommitMessageField, }, directives: { tooltip, @@ -38,15 +40,9 @@ export default { 'changedFiles', ]), ...mapState('commit', ['commitMessage', 'submitCommitLoading']), - ...mapGetters('commit', [ - 'commitButtonDisabled', - 'discardDraftButtonDisabled', - 'branchName', - ]), + ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled', 'branchName']), statusSvg() { - return this.lastCommitMsg - ? this.committedStateSvgPath - : this.noChangesStateSvgPath; + return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath; }, }, methods: { @@ -64,9 +60,7 @@ export default { }); }, forceCreateNewBranch() { - return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => - this.commitChanges(), - ); + return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges()); }, }, }; @@ -105,16 +99,10 @@ export default { @submit.prevent.stop="commitChanges" v-if="!rightPanelCollapsed" > - <div class="multi-file-commit-fieldset"> - <textarea - class="form-control multi-file-commit-message" - name="commit-message" - :value="commitMessage" - :placeholder="__('Write a commit message...')" - @input="updateCommitMessage($event.target.value)" - > - </textarea> - </div> + <commit-message-field + :text="commitMessage" + @input="updateCommitMessage" + /> <div class="clearfix prepend-top-15"> <actions /> <loading-button diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index e4e54f967aa..09ee9d39d43 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -6,3 +6,7 @@ export const UP_KEY_CODE = 38; export const DOWN_KEY_CODE = 40; export const ENTER_KEY_CODE = 13; export const ESC_KEY_CODE = 27; + +// Commit message textarea +export const MAX_TITLE_LENGTH = 50; +export const MAX_BODY_LENGTH = 72; 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/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index b3882cb8d21..4eb23b2ee0f 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -5,45 +5,71 @@ import * as types from '../mutation_types'; export const getProjectData = ( { commit, state, dispatch }, { namespace, projectId, force = false } = {}, -) => new Promise((resolve, reject) => { - if (!state.projects[`${namespace}/${projectId}`] || force) { - commit(types.TOGGLE_LOADING, { entry: state }); - service.getProjectData(namespace, projectId) - .then(res => res.data) - .then((data) => { +) => + new Promise((resolve, reject) => { + if (!state.projects[`${namespace}/${projectId}`] || force) { commit(types.TOGGLE_LOADING, { entry: state }); - commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data }); - if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); - resolve(data); - }) - .catch(() => { - flash('Error loading project data. Please try again.', 'alert', document, null, false, true); - reject(new Error(`Project not loaded ${namespace}/${projectId}`)); - }); - } else { - resolve(state.projects[`${namespace}/${projectId}`]); - } -}); + service + .getProjectData(namespace, projectId) + .then(res => res.data) + .then(data => { + commit(types.TOGGLE_LOADING, { entry: state }); + commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data }); + if (!state.currentProjectId) + commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); + resolve(data); + }) + .catch(() => { + flash( + 'Error loading project data. Please try again.', + 'alert', + document, + null, + false, + true, + ); + reject(new Error(`Project not loaded ${namespace}/${projectId}`)); + }); + } else { + resolve(state.projects[`${namespace}/${projectId}`]); + } + }); export const getBranchData = ( { commit, state, dispatch }, { projectId, branchId, force = false } = {}, -) => new Promise((resolve, reject) => { - if ((typeof state.projects[`${projectId}`] === 'undefined' || - !state.projects[`${projectId}`].branches[branchId]) - || force) { - service.getBranchData(`${projectId}`, branchId) - .then(({ data }) => { - const { id } = data.commit; - commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data }); - commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); - resolve(data); - }) - .catch(() => { - flash('Error loading branch data. Please try again.', 'alert', document, null, false, true); - reject(new Error(`Branch not loaded - ${projectId}/${branchId}`)); - }); - } else { - resolve(state.projects[`${projectId}`].branches[branchId]); - } -}); +) => + new Promise((resolve, reject) => { + if ( + typeof state.projects[`${projectId}`] === 'undefined' || + !state.projects[`${projectId}`].branches[branchId] || + force + ) { + service + .getBranchData(`${projectId}`, branchId) + .then(({ data }) => { + const { id } = data.commit; + commit(types.SET_BRANCH, { + projectPath: `${projectId}`, + branchName: branchId, + branch: data, + }); + commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); + commit(types.SET_CURRENT_BRANCH, branchId); + resolve(data); + }) + .catch(() => { + flash( + 'Error loading branch data. Please try again.', + 'alert', + document, + null, + false, + true, + ); + reject(new Error(`Branch not loaded - ${projectId}/${branchId}`)); + }); + } else { + resolve(state.projects[`${projectId}`].branches[branchId]); + } + }); 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/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 b0573510ff9..2121907dff0 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -19,7 +19,6 @@ import AjaxCache from '~/lib/utils/ajax_cache'; import Vue from 'vue'; import syntaxHighlight from '~/syntax_highlight'; import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; -import { __ } from '~/locale'; import axios from './lib/utils/axios_utils'; import { getLocationHash } from './lib/utils/url_utility'; import Flash from './flash'; @@ -198,6 +197,8 @@ export default class Notes { ); this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff); + this.$wrapperEl.on('click', '.js-toggle-lazy-diff-retry-button', this.onClickRetryLazyLoad.bind(this)); + // fetch notes when tab becomes visible this.$wrapperEl.on('visibilitychange', this.visibilityChange); // when issue status changes, we need to refresh data @@ -244,6 +245,7 @@ export default class Notes { this.$wrapperEl.off('click', '.js-comment-resolve-button'); this.$wrapperEl.off('click', '.system-note-commit-list-toggler'); this.$wrapperEl.off('click', '.js-toggle-lazy-diff'); + this.$wrapperEl.off('click', '.js-toggle-lazy-diff-retry-button'); this.$wrapperEl.off('ajax:success', '.js-main-target-form'); this.$wrapperEl.off('ajax:success', '.js-discussion-note-form'); this.$wrapperEl.off('ajax:complete', '.js-main-target-form'); @@ -1431,16 +1433,15 @@ export default class Notes { syntaxHighlight(fileHolder); } - static renderDiffError($container) { - $container.find('.line_content').html( - $(` - <div class="nothing-here-block"> - ${__( - 'Unable to load the diff.', - )} <a class="js-toggle-lazy-diff" href="javascript:void(0)">Try again</a>? - </div> - `), - ); + onClickRetryLazyLoad(e) { + const $retryButton = $(e.currentTarget); + + $retryButton.prop('disabled', true); + + return this.loadLazyDiff(e) + .then(() => { + $retryButton.prop('disabled', false); + }); } loadLazyDiff(e) { @@ -1449,20 +1450,35 @@ export default class Notes { $container.find('.js-toggle-lazy-diff').removeClass('js-toggle-lazy-diff'); - const tableEl = $container.find('tbody'); - if (tableEl.length === 0) return; + const $tableEl = $container.find('tbody'); + if ($tableEl.length === 0) return; const fileHolder = $container.find('.file-holder'); const url = fileHolder.data('linesPath'); - axios + const $errorContainer = $container.find('.js-error-lazy-load-diff'); + const $successContainer = $container.find('.js-success-lazy-load'); + + /** + * We only fetch resolved discussions. + * Unresolved discussions don't have an endpoint being provided. + */ + if (url) { + return axios .get(url) .then(({ data }) => { + // Reset state in case last request returned error + $successContainer.removeClass('hidden'); + $errorContainer.addClass('hidden'); + Notes.renderDiffContent($container, data); }) .catch(() => { - Notes.renderDiffError($container); + $successContainer.addClass('hidden'); + $errorContainer.removeClass('hidden'); }); + } + return Promise.resolve(); } toggleCommitList(e) { 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/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/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 6b89387ab5f..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; + } } } @@ -485,3 +503,7 @@ fieldset[disabled] .btn, @extend %disabled; } } + +.btn-no-padding { + padding: 0; +} diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index a8f7caca339..6952f356128 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -485,7 +485,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/commits.scss b/app/assets/stylesheets/pages/commits.scss index 86cdda0359e..e9384d41e00 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -180,10 +180,6 @@ justify-content: space-between; align-items: center; flex-grow: 1; - - .merge-request-branches & { - flex-direction: column; - } } .commit-content { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 7f037582ca0..11052be40a8 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -160,6 +160,11 @@ } } } + + .diff-loading-error-block { + padding: $gl-padding * 2 $gl-padding; + text-align: center; + } } .image { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 4692d0fb873..66db4917178 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -762,3 +762,20 @@ max-width: 100%; } } + +// Hack alert: we've rewritten `btn` class in a way that +// we've broken it and it is not possible to use with `btn-link` +// which causes a blank button when it's disabled and hovering +// The css in here is the boostrap one +.btn-link-retry { + &[disabled] { + cursor: not-allowed; + box-shadow: none; + opacity: .65; + + &:hover { + color: $file-mode-changed; + text-decoration: none; + } + } +} 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/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 2c840cb407a..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 { diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index f826f934abf..5381bdc2105 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -663,11 +663,6 @@ } } -.multi-file-commit-message.form-control { - height: 160px; - resize: none; -} - .dirty-diff { // !important need to override monaco inline style width: 4px !important; @@ -860,3 +855,74 @@ font-weight: $gl-font-weight-bold; } } + +.ide-commit-message-field { + height: 200px; + background-color: $white-light; + + .md-area { + display: flex; + flex-direction: column; + height: 100%; + } + + .nav-links { + height: 30px; + } + + .help-block { + margin-top: 2px; + color: $blue-500; + cursor: pointer; + } +} + +.ide-commit-message-textarea-container { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + + .note-textarea { + font-family: $monospace_font; + } +} + +.ide-commit-message-highlights-container { + position: absolute; + left: 0; + top: 0; + right: -100px; + bottom: 0; + padding-right: 100px; + pointer-events: none; + z-index: 1; + + .highlights { + white-space: pre-wrap; + word-wrap: break-word; + color: transparent; + } + + mark { + margin-left: -1px; + padding: 0 2px; + border-radius: $border-radius-small; + background-color: $orange-200; + color: transparent; + opacity: 0.6; + } +} + +.ide-commit-message-textarea { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + width: 100%; + height: 100%; + z-index: 2; + background: transparent; + resize: none; +} 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/application_controller.rb b/app/controllers/application_controller.rb index 24651dd392c..0fdd4d2cb47 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -5,6 +5,7 @@ class ApplicationController < ActionController::Base include Gitlab::GonHelper include GitlabRoutingHelper include PageLayoutHelper + include SafeParamsHelper include SentryHelper include WorkhorseHelper include EnforcesTwoFactorAuthentication diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index ad4e936a3d4..0c34e49206a 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -217,7 +217,7 @@ module NotesActions def note_project strong_memoize(:note_project) do - return nil unless project + next nil unless project note_project_id = params[:note_project_id] @@ -228,7 +228,7 @@ module NotesActions project end - return access_denied! unless can?(current_user, :create_note, the_project) + next access_denied! unless can?(current_user, :create_note, the_project) the_project 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/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index e89eaf7edda..f9e8fe624e8 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -86,7 +86,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController out_of_range = todos.current_page > total_pages if out_of_range - redirect_to url_for(params.merge(page: total_pages, only_path: true)) + redirect_to url_for(safe_params.merge(page: total_pages, only_path: true)) end out_of_range diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb index 6142e75b4c1..4d8a20de017 100644 --- a/app/controllers/groups/variables_controller.rb +++ b/app/controllers/groups/variables_controller.rb @@ -15,7 +15,7 @@ module Groups def update if @group.update(group_variables_params) respond_to do |format| - format.json { return render_group_variables } + format.json { render_group_variables } end else respond_to do |format| diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 5ac4b8710e2..79fa5818359 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -189,6 +189,6 @@ class GroupsController < Groups::ApplicationController params[:id] = group.to_param - url_for(params) + url_for(safe_params) end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 54e7d81de6a..62b739918e6 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -60,13 +60,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end format.patch do - return render_404 unless @merge_request.diff_refs + break render_404 unless @merge_request.diff_refs send_git_patch @project.repository, @merge_request.diff_refs end format.diff do - return render_404 unless @merge_request.diff_refs + break render_404 unless @merge_request.diff_refs send_git_diff @project.repository, @merge_request.diff_refs end 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/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index 517d0b026c2..bf09ea7e4d8 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -12,7 +12,7 @@ class Projects::VariablesController < Projects::ApplicationController def update if @project.update(variables_params) respond_to do |format| - format.json { return render_variables } + format.json { render_variables } end else respond_to do |format| diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 37f14230196..a93b116c6fe 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -404,7 +404,7 @@ class ProjectsController < Projects::ApplicationController params[:namespace_id] = project.namespace.to_param params[:id] = project.to_param - url_for(params) + url_for(safe_params) end def project_export_enabled 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/controllers/users_controller.rb b/app/controllers/users_controller.rb index 956df4a0a16..31f47a7aa7c 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -146,6 +146,6 @@ class UsersController < ApplicationController end def build_canonical_path(user) - url_for(params.merge(username: user.to_param)) + url_for(safe_params.merge(username: user.to_param)) end end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 866b8773db6..fef29789832 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -259,7 +259,7 @@ module BlobHelper options = [] if error == :collapsed - options << link_to('load it anyway', url_for(params.merge(viewer: viewer.type, expanded: true, format: nil))) + options << link_to('load it anyway', url_for(safe_params.merge(viewer: viewer.type, expanded: true, format: nil))) end # If the error is `:server_side_but_stored_externally`, the simple viewer will show the same error, diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index b5ca39711bc..1bb82fd8150 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -180,7 +180,7 @@ module DiffHelper private def diff_btn(title, name, selected) - params_copy = params.dup + params_copy = safe_params.dup params_copy[:view] = name # Always use HTML to handle case where JSON diff rendered this button 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/safe_params_helper.rb b/app/helpers/safe_params_helper.rb new file mode 100644 index 00000000000..b568e8810cc --- /dev/null +++ b/app/helpers/safe_params_helper.rb @@ -0,0 +1,11 @@ +module SafeParamsHelper + # Rails 5.0 requires to permit `params` if they're used in url helpers. + # Use this helper when generating links with `params.merge(...)` + def safe_params + if params.respond_to?(:permit!) + params.except(:host, :port, :protocol).permit! + else + params + end + end +end 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/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index b33131becd3..392cc0bee03 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -6,6 +6,12 @@ module Emails mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason)) end + def issue_due_email(recipient_id, issue_id, reason = nil) + setup_issue_mail(issue_id, recipient_id) + + mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason)) + end + def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil) setup_issue_mail(issue_id, recipient_id) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason)) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 4aa65bf4273..1c8d9ca4aa5 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -162,7 +162,7 @@ module Ci build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies') end - before_transition pending: :running do |build| + after_transition pending: :running do |build| build.ensure_metadata.update_timeout_state end end @@ -479,7 +479,7 @@ module Ci def user_variables Gitlab::Ci::Variables::Collection.new.tap do |variables| - return variables if user.blank? + break variables if user.blank? variables.append(key: 'GITLAB_USER_ID', value: user.id.to_s) variables.append(key: 'GITLAB_USER_EMAIL', value: user.email) @@ -594,7 +594,7 @@ module Ci def persisted_variables Gitlab::Ci::Variables::Collection.new.tap do |variables| - return variables unless persisted? + break variables unless persisted? variables .append(key: 'CI_JOB_ID', value: id.to_s) @@ -643,7 +643,7 @@ module Ci def persisted_environment_variables Gitlab::Ci::Variables::Collection.new.tap do |variables| - return variables unless persisted? && persisted_environment.present? + break variables unless persisted? && persisted_environment.present? variables.concat(persisted_environment.predefined_variables) 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/commit_status.rb b/app/models/commit_status.rb index 3469d5d795c..b6276c2fb50 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -87,7 +87,7 @@ class CommitStatus < ActiveRecord::Base transition [:created, :pending, :running, :manual] => :canceled end - before_transition created: [:pending, :running] do |commit_status| + before_transition [:created, :skipped, :manual] => :pending do |commit_status| commit_status.queued_at = Time.now end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 4ae5dd8c677..db8cf322ef7 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -11,7 +11,9 @@ module CacheMarkdownField extend ActiveSupport::Concern # Increment this number every time the renderer changes its output - CACHE_VERSION = 3 + CACHE_REDCARPET_VERSION = 3 + CACHE_COMMONMARK_VERSION_START = 10 + CACHE_COMMONMARK_VERSION = 11 # changes to these attributes cause the cache to be invalidates INVALIDATED_BY = %w[author project].freeze @@ -49,12 +51,14 @@ module CacheMarkdownField # Always include a project key, or Banzai complains project = self.project if self.respond_to?(:project) - group = self.group if self.respond_to?(:group) + group = self.group if self.respond_to?(:group) context = cached_markdown_fields[field].merge(project: project, group: group) # Banzai is less strict about authors, so don't always have an author key context[:author] = self.author if self.respond_to?(:author) + context[:markdown_engine] = markdown_engine + context end @@ -69,7 +73,7 @@ module CacheMarkdownField Banzai::Renderer.cacheless_render_field(self, markdown_field, options) ] end.to_h - updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION + updates['cached_markdown_version'] = latest_cached_markdown_version updates.each {|html_field, data| write_attribute(html_field, data) } end @@ -90,7 +94,7 @@ module CacheMarkdownField markdown_changed = attribute_changed?(markdown_field) || false html_changed = attribute_changed?(html_field) || false - CacheMarkdownField::CACHE_VERSION == cached_markdown_version && + latest_cached_markdown_version == cached_markdown_version && (html_changed || markdown_changed == html_changed) end @@ -109,6 +113,24 @@ module CacheMarkdownField __send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend end + def latest_cached_markdown_version + return CacheMarkdownField::CACHE_REDCARPET_VERSION unless cached_markdown_version + + if cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START + CacheMarkdownField::CACHE_REDCARPET_VERSION + else + CacheMarkdownField::CACHE_COMMONMARK_VERSION + end + end + + def markdown_engine + if latest_cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START + :redcarpet + else + :common_mark + end + end + included do cattr_reader :cached_markdown_fields do FieldData.new 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/issue.rb b/app/models/issue.rb index c1ffe6512ea..702bfc77803 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -49,6 +49,7 @@ class Issue < ActiveRecord::Base scope :without_due_date, -> { where(due_date: nil) } scope :due_before, ->(date) { where('issues.due_date < ?', date) } scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) } + scope :due_tomorrow, -> { where(due_date: Date.tomorrow) } scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') } scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') } diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index b3ffad00a07..2c3580bbdc6 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -83,14 +83,14 @@ class NotificationRecipient def has_access? DeclarativePolicy.subject_scope do - return false unless user.can?(:receive_notifications) - return true if @skip_read_ability + break false unless user.can?(:receive_notifications) + break true if @skip_read_ability - return false if @target && !user.can?(:read_cross_project) - return false if @project && !user.can?(:read_project, @project) + break false if @target && !user.can?(:read_cross_project) + break false if @project && !user.can?(:read_project, @project) - return true unless read_ability - return true unless DeclarativePolicy.has_policy?(@target) + break true unless read_ability + break true unless DeclarativePolicy.has_policy?(@target) user.can?(read_ability, @target) end diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index f6d9b0215fc..9195408551f 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -47,7 +47,8 @@ class NotificationSetting < ActiveRecord::Base ].freeze EXCLUDED_WATCHER_EVENTS = [ - :push_to_merge_request + :push_to_merge_request, + :issue_due ].push(*EXCLUDED_PARTICIPATING_EVENTS).freeze def self.find_or_create_for(source) diff --git a/app/models/project.rb b/app/models/project.rb index ffd78d3ab70..38139a9b137 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1637,7 +1637,7 @@ class Project < ActiveRecord::Base def container_registry_variables Gitlab::Ci::Variables::Collection.new.tap do |variables| - return variables unless Gitlab.config.registry.enabled + break variables unless Gitlab.config.registry.enabled variables.append(key: 'CI_REGISTRY', value: Gitlab.config.registry.host_port) 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/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index e09b445636f..c289b44f885 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -4,6 +4,9 @@ module Ci class RegisterJobService attr_reader :runner + JOB_QUEUE_DURATION_SECONDS_BUCKETS = [1, 3, 10, 30].freeze + JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET = 5.freeze + Result = Struct.new(:build, :valid?) def initialize(runner) @@ -30,7 +33,7 @@ module Ci end end - builds.find do |build| + builds.auto_include(false).find do |build| next unless runner.can_pick?(build) begin @@ -41,7 +44,7 @@ module Ci build.run! register_success(build) - return Result.new(build, true) + return Result.new(build, true) # rubocop:disable Cop/AvoidReturnFromBlocks rescue Ci::Build::MissingDependenciesError build.drop!(:missing_dependency_failure) end @@ -104,10 +107,22 @@ module Ci end def register_success(job) - job_queue_duration_seconds.observe({ shared_runner: @runner.shared? }, Time.now - job.created_at) + labels = { shared_runner: runner.shared?, + jobs_running_for_project: jobs_running_for_project(job) } + + job_queue_duration_seconds.observe(labels, Time.now - job.queued_at) unless job.queued_at.nil? attempt_counter.increment end + def jobs_running_for_project(job) + return '+Inf' unless runner.shared? + + # excluding currently started job + running_jobs_count = job.project.builds.running.where(runner: Ci::Runner.shared) + .limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1 + running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET ? running_jobs_count : "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+" + end + def failed_attempt_counter @failed_attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_failed_total, "Counts the times a runner tries to register a job") end @@ -117,7 +132,7 @@ module Ci end def job_queue_duration_seconds - @job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time') + @job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time', {}, JOB_QUEUE_DURATION_SECONDS_BUCKETS) end 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/clusters/gcp/verify_provision_status_service.rb b/app/services/clusters/gcp/verify_provision_status_service.rb index f994aacd086..7cc4324677e 100644 --- a/app/services/clusters/gcp/verify_provision_status_service.rb +++ b/app/services/clusters/gcp/verify_provision_status_service.rb @@ -17,7 +17,7 @@ module Clusters when 'DONE' finalize_creation else - return provider.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}") + provider.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}") end end end diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb index 88dfb7a4a90..7e5a77fb056 100644 --- a/app/services/create_deployment_service.rb +++ b/app/services/create_deployment_service.rb @@ -19,8 +19,8 @@ class CreateDeploymentService environment.fire_state_event(action) - return unless environment.save - return if environment.stopped? + break unless environment.save + break if environment.stopped? deploy.tap(&:update_merge_request_metrics!) end diff --git a/app/services/import_export_clean_up_service.rb b/app/services/import_export_clean_up_service.rb index 6442406d77e..74088b970c9 100644 --- a/app/services/import_export_clean_up_service.rb +++ b/app/services/import_export_clean_up_service.rb @@ -10,7 +10,7 @@ class ImportExportCleanUpService def execute Gitlab::Metrics.measure(:import_export_clean_up) do - return unless File.directory?(path) + next unless File.directory?(path) clean_up_export_files end diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index b82d9c64296..83e59a649b6 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -203,10 +203,11 @@ module NotificationRecipientService attr_reader :action attr_reader :previous_assignee attr_reader :skip_current_user - def initialize(target, current_user, action:, previous_assignee: nil, skip_current_user: true) + def initialize(target, current_user, action:, custom_action: nil, previous_assignee: nil, skip_current_user: true) @target = target @current_user = current_user @action = action + @custom_action = custom_action @previous_assignee = previous_assignee @skip_current_user = skip_current_user end @@ -236,7 +237,13 @@ module NotificationRecipientService add_mentions(current_user, target: target) # Add the assigned users, if any - assignees = custom_action == :new_issue ? target.assignees : target.assignee + assignees = case custom_action + when :new_issue + target.assignees + else + target.assignee + end + # We use the `:participating` notification level in order to match existing legacy behavior as captured # in existing specs (notification_service_spec.rb ~ line 507) add_recipients(assignees, :participating, NotificationReason::ASSIGNED) if assignees diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index f94c76cf3ac..274161df946 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -373,6 +373,20 @@ class NotificationService end end + def issue_due(issue) + recipients = NotificationRecipientService.build_recipients( + issue, + issue.author, + action: 'due', + custom_action: :issue_due, + skip_current_user: false + ) + + recipients.each do |recipient| + mailer.send(:issue_due_email, recipient.user.id, issue.id, recipient.reason).deliver_later + end + end + protected def new_resource_email(target, method) diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index aa14206db3b..44e869851ca 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -137,7 +137,7 @@ module Projects return true unless Gitlab.config.registry.enabled ContainerRepository.build_root_repository(project).tap do |repository| - return repository.has_tags? ? repository.delete_tags! : true + break repository.has_tags? ? repository.delete_tags! : true end end diff --git a/app/services/repository_archive_clean_up_service.rb b/app/services/repository_archive_clean_up_service.rb index aa84d36a206..9a88459b511 100644 --- a/app/services/repository_archive_clean_up_service.rb +++ b/app/services/repository_archive_clean_up_service.rb @@ -10,7 +10,7 @@ class RepositoryArchiveCleanUpService def execute Gitlab::Metrics.measure(:repository_archive_clean_up) do - return unless File.directory?(path) + next unless File.directory?(path) clean_up_old_archives clean_up_empty_directories diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 00bf5434b7f..958ef065012 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -159,7 +159,7 @@ module SystemNoteService body = if noteable.time_estimate == 0 "removed time estimate" else - "changed time estimate to #{parsed_time}" + "changed time estimate to #{parsed_time}," end create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking')) diff --git a/app/services/test_hooks/base_service.rb b/app/services/test_hooks/base_service.rb index e9aefb1fb75..aadc1ea644b 100644 --- a/app/services/test_hooks/base_service.rb +++ b/app/services/test_hooks/base_service.rb @@ -19,7 +19,7 @@ module TestHooks error_message = catch(:validation_error) do sample_data = self.__send__(trigger_data_method) # rubocop:disable GitlabSecurity/PublicSend - return hook.execute(sample_data, trigger_key) + return hook.execute(sample_data, trigger_key) # rubocop:disable Cop/AvoidReturnFromBlocks end error(error_message) 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/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder index 70ec6bc6257..d7b6fb9a4a1 100644 --- a/app/views/dashboard/issues.atom.builder +++ b/app/views/dashboard/issues.atom.builder @@ -1,5 +1,5 @@ xml.title "#{current_user.name} issues" -xml.link href: url_for(params), rel: "self", type: "application/atom+xml" +xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml" xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html" xml.id issues_dashboard_url xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index bb472b4c900..4bf04dadf01 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -2,12 +2,12 @@ - page_title _("Issues") - @breadcrumb_link = issues_dashboard_path(assignee_id: current_user.id) = content_for :meta_tags do - = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues") + = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{current_user.name} issues") .top-area = render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set .nav-controls - = link_to params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do + = link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do = icon('rss') = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml index 8680ec2e298..646e89e9bd1 100644 --- a/app/views/discussions/_diff_with_notes.html.haml +++ b/app/views/discussions/_diff_with_notes.html.haml @@ -7,7 +7,7 @@ - unless expanded - diff_data = { lines_path: project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion) } -.diff-file.file-holder{ class: diff_file_class, data: diff_data } +.diff-file.file-holder.js-lazy-load-discussion{ class: diff_file_class, data: diff_data } .js-file-title.file-title.file-title-flex-parent .file-header-content = render "projects/diffs/file_header", diff_file: diff_file, url: discussion_path(discussion), show_toggle: false @@ -28,8 +28,11 @@ %tr.line_holder.line-holder-placeholder %td.old_line.diff-line-num %td.new_line.diff-line-num - %td.line_content + %td.line_content.js-success-lazy-load .js-code-placeholder + %td.js-error-lazy-load-diff.hidden.diff-loading-error-block + - button = button_tag(_("Try again"), class: "btn-link btn-link-retry btn-no-padding js-toggle-lazy-diff-retry-button") + = _("Unable to load the diff. %{button_try_again}").html_safe % { button_try_again: button} = render "discussions/diff_discussion", discussions: [discussion], expanded: true - else - partial = (diff_file.new_file? || diff_file.deleted_file?) ? 'single_image_diff' : 'replaced_image_diff' diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder index a239ea8caf0..2a385b661e5 100644 --- a/app/views/groups/issues.atom.builder +++ b/app/views/groups/issues.atom.builder @@ -1,5 +1,5 @@ xml.title "#{@group.name} issues" -xml.link href: url_for(params), rel: "self", type: "application/atom+xml" +xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml" xml.link href: issues_group_url, rel: "alternate", type: "text/html" xml.id issues_group_url xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 36df03302e8..bbfbea4ac7a 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -1,6 +1,6 @@ - page_title "Issues" = content_for :meta_tags do - = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues") + = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@group.name} issues") - if group_issues_count(state: 'all').zero? = render 'shared/empty_states/issues', project_select_button: true diff --git a/app/views/notify/issue_due_email.html.haml b/app/views/notify/issue_due_email.html.haml new file mode 100644 index 00000000000..e81144b8fcb --- /dev/null +++ b/app/views/notify/issue_due_email.html.haml @@ -0,0 +1,12 @@ +%p.details + #{link_to @issue.author_name, user_url(@issue.author)}'s issue is due soon. + +- if @issue.assignees.any? + %p + Assignee: #{@issue.assignee_list} +%p + This issue is due on: #{@issue.due_date.to_s(:medium)} + +- if @issue.description + %div + = markdown(@issue.description, pipeline: :email, author: @issue.author) diff --git a/app/views/notify/issue_due_email.text.erb b/app/views/notify/issue_due_email.text.erb new file mode 100644 index 00000000000..3c7a57a8a2e --- /dev/null +++ b/app/views/notify/issue_due_email.text.erb @@ -0,0 +1,7 @@ +The following issue is due on <%= @issue.due_date %>: + +Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> +Author: <%= @issue.author_name %> +Assignee: <%= @issue.assignee_list %> + +<%= @issue.description %> 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/blob/_viewer.html.haml b/app/views/projects/blob/_viewer.html.haml index 3124443b4e4..b9663bbba15 100644 --- a/app/views/projects/blob/_viewer.html.haml +++ b/app/views/projects/blob/_viewer.html.haml @@ -2,13 +2,16 @@ - 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 +- viewer_url = local_assigns.fetch(:viewer_url) { url_for(safe_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) } - if render_error = 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 71176acd12d..d0c01f95cb7 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -29,7 +29,7 @@ = s_('Branches|Cant find HEAD commit for this branch') - if branch.name != @repository.root_ref - .divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind), + .divergence-graph.hidden-xs{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind), default_branch: @repository.root_ref, number_commits_ahead: diverging_count_label(number_commits_ahead) } } .graph-side 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/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 289bfdd69bc..3fd0fa348b3 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -22,7 +22,7 @@ = author_avatar(commit, size: 36) .commit-detail.flex-list - .commit-content + .commit-content.qa-commit-content - if view_details && merge_request = link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "commit-row-message item-title" - else diff --git a/app/views/projects/diffs/_collapsed.html.haml b/app/views/projects/diffs/_collapsed.html.haml index 8772bd4705f..5762f4d86d7 100644 --- a/app/views/projects/diffs/_collapsed.html.haml +++ b/app/views/projects/diffs/_collapsed.html.haml @@ -1,5 +1,5 @@ - diff_file = viewer.diff_file -- url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier)) +- url = url_for(safe_params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier)) .nothing-here-block.diff-collapsed{ data: { diff_for_path: url } } This diff is collapsed. %a.click-to-expand Click to expand it. diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml index dd1a836fa20..297b928f020 100644 --- a/app/views/projects/issues/_nav_btns.html.haml +++ b/app/views/projects/issues/_nav_btns.html.haml @@ -1,4 +1,4 @@ -= link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do += link_to safe_params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do = icon('rss') - if @can_bulk_update = button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle" diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder index 4029926f373..6330245954e 100644 --- a/app/views/projects/issues/index.atom.builder +++ b/app/views/projects/issues/index.atom.builder @@ -1,5 +1,5 @@ xml.title "#{@project.name} issues" -xml.link href: url_for(params), rel: "self", type: "application/atom+xml" +xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml" xml.link href: project_issues_url(@project), rel: "alternate", type: "text/html" xml.id project_issues_url(@project) xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index c427a9eedc2..1e7737aeb97 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -5,7 +5,7 @@ - new_issue_email = @project.new_issuable_address(current_user, 'issue') = content_for :meta_tags do - = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues") + = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@project.name} issues") - if project_issues(@project).exists? %div{ class: (container_class) } diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml index 9d5cebdda53..f81db9b4e28 100644 --- a/app/views/projects/merge_requests/creations/_new_compare.html.haml +++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml @@ -3,7 +3,7 @@ = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f| .hide.alert.alert-danger.mr-compare-errors - .merge-request-branches.js-merge-request-new-compare.row{ 'data-target-project-url': project_new_merge_request_update_branches_path(@source_project), 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) } + .js-merge-request-new-compare.row{ 'data-target-project-url': project_new_merge_request_update_branches_path(@source_project), 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) } .col-md-6 .panel.panel-default.panel-new-merge-request .panel-heading diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml index 376ac377562..68780cedeb1 100644 --- a/app/views/projects/merge_requests/creations/_new_submit.html.haml +++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml @@ -26,16 +26,16 @@ - else %ul.merge-request-tabs.nav-links.no-top.no-bottom %li.commits-tab.active - = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do + = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do Commits %span.badge= @commits.size - if @pipelines.any? %li.builds-tab - = link_to url_for(params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do + = link_to url_for(safe_params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do Pipelines %span.badge= @pipelines.size %li.diffs-tab - = link_to url_for(params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do + = link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do Changes %span.badge= @merge_request.diff_size @@ -46,7 +46,7 @@ -# This tab is always loaded via AJAX - if @pipelines.any? #pipelines.pipelines.tab-pane - = render 'projects/merge_requests/pipelines', endpoint: url_for(params.merge(action: 'pipelines', format: :json)), disable_initialization: true + = render 'projects/merge_requests/pipelines', endpoint: url_for(safe_params.merge(action: 'pipelines', format: :json)), disable_initialization: true .mr-loading-status = spinner diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index 877101b05ca..8f2142af2ce 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -1,24 +1,25 @@ - breadcrumb_title "Pipelines" -- page_title "New Pipeline" +- page_title = s_("Pipeline|Run Pipeline") %h3.page-title - New Pipeline + = s_("Pipeline|Run Pipeline") %hr = form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do |f| = form_errors(@pipeline) .form-group - = f.label :ref, 'Create for', class: 'control-label' + = f.label :ref, s_('Pipeline|Run on'), class: 'control-label' .col-sm-10 = hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch = dropdown_tag(params[:ref] || @project.default_branch, options: { toggle_class: 'js-branch-select wide git-revision-dropdown-toggle', - filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: "Search branches", + filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: s_("Pipeline|Search branches"), data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } }) - .help-block Existing branch name, tag + .help-block + = s_("Pipeline|Existing branch name, tag") .form-actions - = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3 - = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-cancel' + = f.submit s_('Pipeline|Run pipeline'), class: 'btn btn-success', tabindex: 3 + = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-default pull-right' -# haml-lint:disable InlineJavaScript %script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml index 98d56a3e5c5..12ccae10260 100644 --- a/app/views/projects/protected_branches/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml @@ -7,8 +7,8 @@ - content_for :push_access_levels do .push_access_levels-container = dropdown_tag('Select', - options: { toggle_class: 'js-allowed-to-push wide', - dropdown_class: 'dropdown-menu-selectable capitalize-header', + options: { toggle_class: 'js-allowed-to-push qa-allowed-to-push-select wide', + dropdown_class: 'dropdown-menu-selectable qa-allowed-to-push-dropdown capitalize-header', data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }}) = render 'projects/protected_branches/shared/create_protected_branch' diff --git a/app/views/projects/protected_branches/_update_protected_branch.html.haml b/app/views/projects/protected_branches/_update_protected_branch.html.haml index c61b2951e1e..98363f2018a 100644 --- a/app/views/projects/protected_branches/_update_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_update_protected_branch.html.haml @@ -6,5 +6,5 @@ %td = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_levels.first.access_level = dropdown_tag( (protected_branch.push_access_levels.first.humanize || 'Select') , - options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header', + options: { toggle_class: 'js-allowed-to-push qa-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header', data: { field_name: "allowed_to_push_#{protected_branch.id}", access_level_id: protected_branch.push_access_levels.first.id }}) diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml index 300055a4207..d1ed438eb21 100644 --- a/app/views/projects/protected_branches/shared/_branches_list.html.haml +++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml @@ -1,4 +1,4 @@ -.protected-branches-list.js-protected-branches-list +.protected-branches-list.js-protected-branches-list.qa-protected-branches-list - if @protected_branches.empty? .panel-heading %h3.panel-title diff --git a/app/views/projects/protected_branches/shared/_dropdown.html.haml b/app/views/projects/protected_branches/shared/_dropdown.html.haml index 74435236808..b3d6068039a 100644 --- a/app/views/projects/protected_branches/shared/_dropdown.html.haml +++ b/app/views/projects/protected_branches/shared/_dropdown.html.haml @@ -1,8 +1,8 @@ = f.hidden_field(:name) = dropdown_tag('Select branch or create wildcard', - options: { toggle_class: 'js-protected-branch-select js-filter-submit wide git-revision-dropdown-toggle', - filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: "Search protected branches", + options: { toggle_class: 'js-protected-branch-select js-filter-submit wide git-revision-dropdown-toggle qa-protected-branch-select', + filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown qa-protected-branch-dropdown", placeholder: "Search protected branches", footer_content: true, data: { show_no: true, show_any: true, show_upcoming: true, selected: params[:protected_branch_name], diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml index 55d87c35a80..fd5c1aa342a 100644 --- a/app/views/projects/protected_branches/shared/_index.html.haml +++ b/app/views/projects/protected_branches/shared/_index.html.haml @@ -4,7 +4,7 @@ .settings-header %h4 Protected Branches - %button.btn.js-settings-toggle{ type: 'button' } + %button.btn.js-settings-toggle.qa-expand-protected-branches{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p Keep stable branches secure and force developers to use merge requests. diff --git a/app/views/projects/protected_branches/shared/_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_protected_branch.html.haml index 10b81e42572..f5b21f0e887 100644 --- a/app/views/projects/protected_branches/shared/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/shared/_protected_branch.html.haml @@ -2,7 +2,7 @@ %tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) } } %td - %span.ref-name= protected_branch.name + %span.ref-name.qa-protected-branch-name= protected_branch.name - if @project.root_ref?(protected_branch.name) %span.label.label-info.prepend-left-5 default 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/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index 4c8c92d722a..f1c39b9e923 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -8,8 +8,8 @@ - @options && @options.each do |key, value| = hidden_field_tag key, value, id: nil .dropdown - = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" } - .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } + = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown qa-branches-select" } + .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging.qa-branches-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } .dropdown-page-one = dropdown_title _("Switch branch/tag") = dropdown_filter _("Search branches and tags") 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/_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/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')}'); diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 9a11cdb121e..9aea3bad27b 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -18,6 +18,7 @@ - cronjob:stuck_import_jobs - cronjob:stuck_merge_jobs - cronjob:trending_projects +- cronjob:issue_due_scheduler - gcp_cluster:cluster_install_app - gcp_cluster:cluster_provision @@ -39,6 +40,8 @@ - github_importer:github_import_stage_import_pull_requests - github_importer:github_import_stage_import_repository +- mail_scheduler:mail_scheduler_issue_due + - object_storage_upload - object_storage:object_storage_background_move - object_storage:object_storage_migrate_uploads diff --git a/app/workers/concerns/mail_scheduler_queue.rb b/app/workers/concerns/mail_scheduler_queue.rb new file mode 100644 index 00000000000..9df55ad9522 --- /dev/null +++ b/app/workers/concerns/mail_scheduler_queue.rb @@ -0,0 +1,7 @@ +module MailSchedulerQueue + extend ActiveSupport::Concern + + included do + queue_namespace :mail_scheduler + end +end diff --git a/app/workers/issue_due_scheduler_worker.rb b/app/workers/issue_due_scheduler_worker.rb new file mode 100644 index 00000000000..16ab5d069e0 --- /dev/null +++ b/app/workers/issue_due_scheduler_worker.rb @@ -0,0 +1,10 @@ +class IssueDueSchedulerWorker + include ApplicationWorker + include CronjobQueue + + def perform + project_ids = Issue.opened.due_tomorrow.group(:project_id).pluck(:project_id).map { |id| [id] } + + MailScheduler::IssueDueWorker.bulk_perform_async(project_ids) + end +end diff --git a/app/workers/mail_scheduler/issue_due_worker.rb b/app/workers/mail_scheduler/issue_due_worker.rb new file mode 100644 index 00000000000..b06079d68ca --- /dev/null +++ b/app/workers/mail_scheduler/issue_due_worker.rb @@ -0,0 +1,14 @@ +module MailScheduler + class IssueDueWorker + include ApplicationWorker + include MailSchedulerQueue + + def perform(project_id) + notification_service = NotificationService.new + + Issue.opened.due_tomorrow.in_projects(project_id).preload(:project).find_each do |issue| + notification_service.issue_due(issue) + end + end + end +end diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 3909dbf7d7f..f88b3fdbfb1 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -33,7 +33,7 @@ class PostReceive unless @user log("Triggered hook for non-existing user \"#{post_received.identifier}\"") - return false + return false # rubocop:disable Cop/AvoidReturnFromBlocks end if Gitlab::Git.tag_ref?(ref) diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb index fb26fa4c515..7ebf69bdc39 100644 --- a/app/workers/stuck_ci_jobs_worker.rb +++ b/app/workers/stuck_ci_jobs_worker.rb @@ -38,7 +38,7 @@ class StuckCiJobsWorker def drop_stuck(status, timeout) search(status, timeout) do |build| - return unless build.stuck? + break unless build.stuck? drop_build :stuck, build, status, timeout end |