diff options
author | Pawel Chojnacki <pawel@chojnacki.ws> | 2017-06-22 16:05:52 +0300 |
---|---|---|
committer | Pawel Chojnacki <pawel@chojnacki.ws> | 2017-06-22 16:05:52 +0300 |
commit | 97c42df3b804a37e659c3cda6bd8a52570f31366 (patch) | |
tree | 97c38db7f71a93a7b0db5ca2c682d6d17479cbdb /app | |
parent | 3833f1dd84dfec844443a5b1d9ba2bd2b911c0bc (diff) | |
parent | 11716f310dcc495600f5a17e08456a1abb296482 (diff) |
Merge remote-tracking branch 'upstream/master' into 28717-additional-metrics-review-branch
Diffstat (limited to 'app')
212 files changed, 1740 insertions, 1694 deletions
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index b37698fe9ca..3f083655f95 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -11,7 +11,6 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { // Issue boards is slightly different, we handle all the requests async // instead or reloading the page, we just re-fire the list ajax requests this.isHandledAsync = true; - this.cantEdit = cantEdit; } diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index 86d99dd87da..2c38440a2af 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -1,29 +1,30 @@ -/* eslint-disable no-param-reassign */ - import Vue from 'vue'; -import VueResource from 'vue-resource'; -import CommitPipelinesTable from './pipelines_table'; - -Vue.use(VueResource); +import commitPipelinesTable from './pipelines_table.vue'; /** - * Commits View > Pipelines Tab > Pipelines Table. - * - * Renders Pipelines table in pipelines tab in the commits show view. + * Used in: + * - Commit details View > Pipelines Tab > Pipelines Table. + * - Merge Request details View > Pipelines Tab > Pipelines Table. + * - New Merge Request View > Pipelines Tab > Pipelines Table. */ -// export for use in merge_request_tabs.js (TODO: remove this hack) +const CommitPipelinesTable = Vue.extend(commitPipelinesTable); + +// export for use in merge_request_tabs.js (TODO: remove this hack when we understand how to load +// vue.js in merge_request_tabs.js) window.gl = window.gl || {}; window.gl.CommitPipelinesTable = CommitPipelinesTable; -$(() => { - gl.commits = gl.commits || {}; - gl.commits.pipelines = gl.commits.pipelines || {}; - +document.addEventListener('DOMContentLoaded', () => { const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { - gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable().$mount(); - pipelineTableViewEl.appendChild(gl.commits.pipelines.PipelinesTableBundle.$el); + const table = new CommitPipelinesTable({ + propsData: { + endpoint: pipelineTableViewEl.dataset.endpoint, + helpPagePath: pipelineTableViewEl.dataset.helpPagePath, + }, + }).$mount(); + pipelineTableViewEl.appendChild(table.$el); } }); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js deleted file mode 100644 index 70ba83ce5b9..00000000000 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js +++ /dev/null @@ -1,191 +0,0 @@ -import Vue from 'vue'; -import Visibility from 'visibilityjs'; -import pipelinesTableComponent from '../../vue_shared/components/pipelines_table.vue'; -import PipelinesService from '../../pipelines/services/pipelines_service'; -import PipelineStore from '../../pipelines/stores/pipelines_store'; -import eventHub from '../../pipelines/event_hub'; -import emptyState from '../../pipelines/components/empty_state.vue'; -import errorState from '../../pipelines/components/error_state.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import '../../lib/utils/common_utils'; -import '../../vue_shared/vue_resource_interceptor'; -import Poll from '../../lib/utils/poll'; - -/** - * - * Uses `pipelines-table-component` to render Pipelines table with an API call. - * Endpoint is provided in HTML and passed as `endpoint`. - * We need a store to store the received environemnts. - * We need a service to communicate with the server. - * - */ - -export default Vue.component('pipelines-table', { - - components: { - pipelinesTableComponent, - errorState, - emptyState, - loadingIcon, - }, - - /** - * Accesses the DOM to provide the needed data. - * Returns the necessary props to render `pipelines-table-component` component. - * - * @return {Object} - */ - data() { - const store = new PipelineStore(); - - return { - endpoint: null, - helpPagePath: null, - store, - state: store.state, - isLoading: false, - hasError: false, - isMakingRequest: false, - updateGraphDropdown: false, - hasMadeRequest: false, - }; - }, - - computed: { - shouldRenderErrorState() { - return this.hasError && !this.isLoading; - }, - - /** - * Empty state is only rendered if after the first request we receive no pipelines. - * - * @return {Boolean} - */ - shouldRenderEmptyState() { - return !this.state.pipelines.length && - !this.isLoading && - this.hasMadeRequest && - !this.hasError; - }, - - shouldRenderTable() { - return !this.isLoading && - this.state.pipelines.length > 0 && - !this.hasError; - }, - }, - - /** - * When the component is about to be mounted, tell the service to fetch the data - * - * A request to fetch the pipelines will be made. - * In case of a successfull response we will store the data in the provided - * store, in case of a failed response we need to warn the user. - * - */ - beforeMount() { - const element = document.querySelector('#commit-pipeline-table-view'); - - this.endpoint = element.dataset.endpoint; - this.helpPagePath = element.dataset.helpPagePath; - this.service = new PipelinesService(this.endpoint); - - this.poll = new Poll({ - resource: this.service, - method: 'getPipelines', - successCallback: this.successCallback, - errorCallback: this.errorCallback, - notificationCallback: this.setIsMakingRequest, - }); - - if (!Visibility.hidden()) { - this.isLoading = true; - this.poll.makeRequest(); - } else { - // If tab is not visible we need to make the first request so we don't show the empty - // state without knowing if there are any pipelines - this.fetchPipelines(); - } - - Visibility.change(() => { - if (!Visibility.hidden()) { - this.poll.restart(); - } else { - this.poll.stop(); - } - }); - - eventHub.$on('refreshPipelines', this.fetchPipelines); - }, - - beforeDestroy() { - eventHub.$off('refreshPipelines'); - }, - - destroyed() { - this.poll.stop(); - }, - - methods: { - fetchPipelines() { - this.isLoading = true; - - return this.service.getPipelines() - .then(response => this.successCallback(response)) - .catch(() => this.errorCallback()); - }, - - successCallback(resp) { - const response = resp.json(); - - this.hasMadeRequest = true; - - // depending of the endpoint the response can either bring a `pipelines` key or not. - const pipelines = response.pipelines || response; - this.store.storePipelines(pipelines); - this.isLoading = false; - this.updateGraphDropdown = true; - }, - - errorCallback() { - this.hasError = true; - this.isLoading = false; - this.updateGraphDropdown = false; - }, - - setIsMakingRequest(isMakingRequest) { - this.isMakingRequest = isMakingRequest; - - if (isMakingRequest) { - this.updateGraphDropdown = false; - } - }, - }, - - template: ` - <div class="content-list pipelines"> - - <loading-icon - label="Loading pipelines" - size="3" - v-if="isLoading" - /> - - <empty-state - v-if="shouldRenderEmptyState" - :help-page-path="helpPagePath" /> - - <error-state v-if="shouldRenderErrorState" /> - - <div - class="table-holder" - v-if="shouldRenderTable"> - <pipelines-table-component - :pipelines="state.pipelines" - :service="service" - :update-graph-dropdown="updateGraphDropdown" - /> - </div> - </div> - `, -}); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue new file mode 100644 index 00000000000..3c77f14d533 --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -0,0 +1,90 @@ +<script> + import PipelinesService from '../../pipelines/services/pipelines_service'; + import PipelineStore from '../../pipelines/stores/pipelines_store'; + import pipelinesMixin from '../../pipelines/mixins/pipelines'; + + export default { + props: { + endpoint: { + type: String, + required: true, + }, + helpPagePath: { + type: String, + required: true, + }, + }, + mixins: [ + pipelinesMixin, + ], + + data() { + const store = new PipelineStore(); + + return { + store, + state: store.state, + }; + }, + + computed: { + /** + * Empty state is only rendered if after the first request we receive no pipelines. + * + * @return {Boolean} + */ + shouldRenderEmptyState() { + return !this.state.pipelines.length && + !this.isLoading && + this.hasMadeRequest && + !this.hasError; + }, + + shouldRenderTable() { + return !this.isLoading && + this.state.pipelines.length > 0 && + !this.hasError; + }, + }, + created() { + this.service = new PipelinesService(this.endpoint); + }, + methods: { + successCallback(resp) { + const response = resp.json(); + + // depending of the endpoint the response can either bring a `pipelines` key or not. + const pipelines = response.pipelines || response; + this.setCommonData(pipelines); + }, + }, + }; +</script> +<template> + <div class="content-list pipelines"> + + <loading-icon + label="Loading pipelines" + size="3" + v-if="isLoading" + /> + + <empty-state + v-if="shouldRenderEmptyState" + :help-page-path="helpPagePath" + /> + + <error-state + v-if="shouldRenderErrorState" + /> + + <div + class="table-holder" + v-if="shouldRenderTable"> + <pipelines-table-component + :pipelines="state.pipelines" + :update-graph-dropdown="updateGraphDropdown" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 5f87a05067b..88b4b567fa9 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -79,7 +79,18 @@ import initSettingsPanels from './settings_panels'; path = page.split(':'); shortcut_handler = null; - new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(); + $('.js-gfm-input').each((i, el) => { + const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); + const enableGFM = gl.utils.convertPermissionToBoolean(el.dataset.supportsAutocomplete); + gfm.setup($(el), { + emojis: true, + members: enableGFM, + issues: enableGFM, + milestones: enableGFM, + mergeRequests: enableGFM, + labels: enableGFM, + }); + }); function initBlob() { new LineHighlighter(); @@ -176,7 +187,7 @@ import initSettingsPanels from './settings_panels'; case 'groups:milestones:update': new ZenMode(); new gl.DueDateSelectors(); - new gl.GLForm($('.milestone-form')); + new gl.GLForm($('.milestone-form'), true); break; case 'projects:compare:show': new gl.Diff(); @@ -188,7 +199,7 @@ import initSettingsPanels from './settings_panels'; case 'projects:issues:new': case 'projects:issues:edit': shortcut_handler = new ShortcutsNavigation(); - new gl.GLForm($('.issue-form')); + new gl.GLForm($('.issue-form'), true); new IssuableForm($('.issue-form')); new LabelsSelect(); new MilestoneSelect(); @@ -199,7 +210,7 @@ import initSettingsPanels from './settings_panels'; case 'projects:merge_requests:edit': new gl.Diff(); shortcut_handler = new ShortcutsNavigation(); - new gl.GLForm($('.merge-request-form')); + new gl.GLForm($('.merge-request-form'), true); new IssuableForm($('.merge-request-form')); new LabelsSelect(); new MilestoneSelect(); @@ -208,22 +219,24 @@ import initSettingsPanels from './settings_panels'; break; case 'projects:tags:new': new ZenMode(); - new gl.GLForm($('.tag-form')); + new gl.GLForm($('.tag-form'), true); new RefSelectDropdown($('.js-branch-select'), window.gl.availableRefs); break; case 'projects:snippets:new': case 'projects:snippets:edit': case 'projects:snippets:create': case 'projects:snippets:update': + new gl.GLForm($('.snippet-form'), true); + break; case 'snippets:new': case 'snippets:edit': case 'snippets:create': case 'snippets:update': - new gl.GLForm($('.snippet-form')); + new gl.GLForm($('.snippet-form'), false); break; case 'projects:releases:edit': new ZenMode(); - new gl.GLForm($('.release-form')); + new gl.GLForm($('.release-form'), true); break; case 'projects:merge_requests:show': new gl.Diff(); @@ -471,7 +484,7 @@ import initSettingsPanels from './settings_panels'; new gl.Wikis(); shortcut_handler = new ShortcutsWiki(); new ZenMode(); - new gl.GLForm($('.wiki-form')); + new gl.GLForm($('.wiki-form'), true); break; case 'snippets': shortcut_handler = new ShortcutsNavigation(); diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 98ddcc20036..73675d300be 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -287,6 +287,10 @@ window.DropzoneInput = (function() { $uploadingErrorMessage.html(message); }; + closeAlertMessage = function() { + return form.find('.div-dropzone-alert').alert('close'); + }; + form.find('.markdown-selector').click(function(e) { e.preventDefault(); $(this).closest('.gfm-form').find('.div-dropzone').click(); diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 809c147bf25..b25113e0fc6 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -403,6 +403,14 @@ export default { return ''; }, + displayEnvironmentActions() { + return this.hasManualActions || + this.externalURL || + this.monitoringUrl || + this.hasStopAction || + this.canRetry; + }, + /** * Constructs folder URL based on the current location and the folder id. * @@ -535,9 +543,12 @@ export default { </span> </div> - <div class="table-section section-30 table-button-footer" role="gridcell"> + <div + v-if="!model.isFolder && displayEnvironmentActions" + class="table-section section-30 table-button-footer" + role="gridcell"> + <div - v-if="!model.isFolder" class="btn-group table-action-buttons" role="group"> diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index 2af242a69df..5838b1bdbb7 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -56,7 +56,7 @@ class DropdownHint extends gl.FilteredSearchDropdown { } renderContent() { - const dropdownData = gl.FilteredSearchTokenKeys.get() + const dropdownData = this.tokenKeys.get() .map(tokenKey => ({ icon: `fa-${tokenKey.icon}`, hint: tokenKey.key, diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 8f547bd8f1f..c7c8d42e677 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -487,6 +487,7 @@ class FilteredSearchManager { } searchState(e) { + e.preventDefault(); const target = e.currentTarget; // remove focus outline after click target.blur(); diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 401dec1a370..105762cb1ba 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -34,7 +34,7 @@ class GfmAutoComplete { const $input = $(input); $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); // This triggers at.js again - // Needed for slash commands with suffixes (ex: /label ~) + // Needed for quick actions with suffixes (ex: /label ~) $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); $input.on('clear-commands-cache.atwho', () => this.clearCache()); }); @@ -48,8 +48,8 @@ class GfmAutoComplete { if (this.enableMap.mergeRequests) this.setupMergeRequests($input); if (this.enableMap.labels) this.setupLabels($input); - // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms - $input.filter('[data-supports-slash-commands="true"]').atwho({ + // We don't instantiate the quick actions autocomplete for note and issue/MR edit forms + $input.filter('[data-supports-quick-actions="true"]').atwho({ at: '/', alias: 'commands', searchKey: 'search', diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index 84bd2e092e6..a8856120c5e 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -22,6 +22,7 @@ export default class IssuableBulkUpdateSidebar { initDomElements() { this.$page = $('.page-with-sidebar'); this.$sidebar = $('.right-sidebar'); + this.$sidebarInnerContainer = this.$sidebar.find('.issuable-sidebar'); this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide'); this.$bulkEditSubmitBtn = $('.update-selected-issues'); this.$bulkUpdateEnableBtn = $('.js-bulk-update-toggle'); @@ -113,6 +114,7 @@ export default class IssuableBulkUpdateSidebar { toggleSidebarDisplay(show) { this.$page.toggleClass(SIDEBAR_EXPANDED_CLASS, show); this.$page.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show); + this.$sidebarInnerContainer.toggleClass(HIDDEN_CLASS, !show); this.$sidebar.toggleClass(SIDEBAR_EXPANDED_CLASS, show); this.$sidebar.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show); } diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index e14414d3f68..3d5fb7f441c 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -51,6 +51,11 @@ export default { required: false, default: '', }, + initialTaskStatus: { + type: String, + required: false, + default: '', + }, updatedAt: { type: String, required: false, @@ -105,6 +110,7 @@ export default { updatedAt: this.updatedAt, updatedByName: this.updatedByName, updatedByPath: this.updatedByPath, + taskStatus: this.initialTaskStatus, }); return { @@ -198,13 +204,7 @@ export default { method: 'getData', successCallback: (res) => { const data = res.json(); - const shouldUpdate = this.store.stateShouldUpdate(data); - this.store.updateState(data); - - if (this.showForm && (shouldUpdate.title || shouldUpdate.description)) { - this.store.formState.lockedWarningVisible = true; - } }, errorCallback(err) { throw new Error(err); diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index 5ae617356e0..43db66c8e08 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -37,23 +37,12 @@ }); }, taskStatus() { - const taskRegexMatches = this.taskStatus.match(/(\d+) of (\d+)/); - const $issuableHeader = $('.issuable-meta'); - const $tasks = $('#task_status', $issuableHeader); - const $tasksShort = $('#task_status_short', $issuableHeader); - - if (taskRegexMatches) { - $tasks.text(this.taskStatus); - $tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`); - } else { - $tasks.text(''); - $tasksShort.text(''); - } + this.updateTaskStatusText(); }, }, methods: { renderGFM() { - $(this.$refs['gfm-entry-content']).renderGFM(); + $(this.$refs['gfm-content']).renderGFM(); if (this.canUpdate) { // eslint-disable-next-line no-new @@ -64,9 +53,24 @@ }); } }, + updateTaskStatusText() { + const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/); + const $issuableHeader = $('.issuable-meta'); + const $tasks = $('#task_status', $issuableHeader); + const $tasksShort = $('#task_status_short', $issuableHeader); + + if (taskRegexMatches) { + $tasks.text(this.taskStatus); + $tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`); + } else { + $tasks.text(''); + $tasksShort.text(''); + } + }, }, mounted() { this.renderGFM(); + this.updateTaskStatusText(); }, }; </script> diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index 30a1be5cb50..54650d2f184 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -41,7 +41,7 @@ <textarea id="issue-description" class="note-textarea js-gfm-input js-autosize markdown-area" - data-supports-slash-commands="false" + data-supports-quick-actionss="false" aria-label="Description" v-model="formState.description" ref="textarea" diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index 14b2a1e18e9..ad8cb6465e2 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -45,6 +45,7 @@ document.addEventListener('DOMContentLoaded', () => { updatedAt: this.updatedAt, updatedByName: this.updatedByName, updatedByPath: this.updatedByPath, + initialTaskStatus: this.initialTaskStatus, }, }); }, diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js index 27c2d349f52..0c8bd6f1cc3 100644 --- a/app/assets/javascripts/issue_show/stores/index.js +++ b/app/assets/javascripts/issue_show/stores/index.js @@ -1,23 +1,6 @@ export default class Store { - constructor({ - titleHtml, - titleText, - descriptionHtml, - descriptionText, - updatedAt, - updatedByName, - updatedByPath, - }) { - this.state = { - titleHtml, - titleText, - descriptionHtml, - descriptionText, - taskStatus: '', - updatedAt, - updatedByName, - updatedByPath, - }; + constructor(initialState) { + this.state = initialState; this.formState = { title: '', confidential: false, @@ -29,6 +12,10 @@ export default class Store { } updateState(data) { + if (this.stateShouldUpdate(data)) { + this.formState.lockedWarningVisible = true; + } + this.state.titleHtml = data.title; this.state.titleText = data.title_text; this.state.descriptionHtml = data.description; @@ -40,10 +27,8 @@ export default class Store { } stateShouldUpdate(data) { - return { - title: this.state.titleText !== data.title_text, - description: this.state.descriptionText !== data.description_text, - }; + return this.state.titleText !== data.title_text || + this.state.descriptionText !== data.description_text; } setFormState(state) { diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 54c0da3fc9c..bfcc50996cc 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -34,7 +34,7 @@ window.dateFormat = dateFormat; w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) { $timeagoEls.each((i, el) => { - el.setAttribute('title', gl.utils.formatDate(el.getAttribute('datetime'))); + el.setAttribute('title', el.getAttribute('title')); if (setTimeago) { // Recreate with custom template diff --git a/app/assets/javascripts/locale/en/app.js b/app/assets/javascripts/locale/en/app.js index 0bb76c80b7a..d634af959e5 100644 --- a/app/assets/javascripts/locale/en/app.js +++ b/app/assets/javascripts/locale/en/app.js @@ -1 +1 @@ -var locales = locales || {}; locales['en'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-04-12 22:36-0500","Last-Translator":"FULL NAME <EMAIL@ADDRESS>","Language-Team":"English","Language":"en","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","lang":"en","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":[""],"Cancel":[""],"Commit":["",""],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":[""],"CycleAnalyticsStage|Code":[""],"CycleAnalyticsStage|Issue":[""],"CycleAnalyticsStage|Plan":[""],"CycleAnalyticsStage|Production":[""],"CycleAnalyticsStage|Review":[""],"CycleAnalyticsStage|Staging":[""],"CycleAnalyticsStage|Test":[""],"Delete":[""],"Deploy":["",""],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":[""],"FirstPushedBy|pushed by":[""],"From issue creation until deploy to production":[""],"From merge request merge until deploy to production":[""],"Interval Pattern":[""],"Introducing Cycle Analytics":[""],"Last %d day":["",""],"Last Pipeline":[""],"Limited to showing %d event at most":["",""],"Median":[""],"New Issue":["",""],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":[""],"Not enough data":[""],"OpenedNDaysAgo|Opened":[""],"Owner":[""],"Pipeline Health":[""],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":[""],"Read more":[""],"Related Commits":[""],"Related Deployed Jobs":[""],"Related Issues":[""],"Related Jobs":[""],"Related Merge Requests":[""],"Related Merged Requests":[""],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["",""],"Target Branch":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":[""],"The collection of events added to the data gathered for that stage.":[""],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":[""],"The phase of the development lifecycle.":[""],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":[""],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":[""],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":[""],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":[""],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":[""],"The time taken by each data entry gathered by that stage.":[""],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":[""],"Time before an issue gets scheduled":[""],"Time before an issue starts implementation":[""],"Time between merge request creation and merge/close":[""],"Time until first merge request":[""],"Time|hr":["",""],"Time|min":["",""],"Time|s":[""],"Total Time":[""],"Total test time for all commits/merges":[""],"Want to see the data? Please ask an administrator for access.":[""],"We don't have enough data to show this stage.":[""],"You need permission.":[""],"day":["",""]}}};
\ No newline at end of file +var locales = locales || {}; locales['en'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-04-12 22:36-0500","Last-Translator":"FULL NAME <EMAIL@ADDRESS>","Language-Team":"English","Language":"en","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","lang":"en","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"%{commit_author_link} committed %{commit_timeago}":[""],"About auto deploy":[""],"Active":[""],"Activity":[""],"Add Changelog":[""],"Add Contribution guide":[""],"Add License":[""],"Add an SSH key to your profile to pull or push via SSH.":[""],"Add new directory":[""],"Archived project! Repository is read-only":[""],"Are you sure you want to delete this pipeline schedule?":[""],"Attach a file by drag & drop or %{upload_link}":[""],"Branch":["",""],"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}":[""],"Branches":[""],"Browse files":[""],"ByAuthor|by":[""],"CI configuration":[""],"Cancel":[""],"ChangeTypeActionLabel|Pick into branch":[""],"ChangeTypeActionLabel|Revert in branch":[""],"ChangeTypeAction|Cherry-pick":[""],"Changelog":[""],"Charts":[""],"Cherry-pick this commit":[""],"Cherry-pick this merge request":[""],"CiStatusLabel|canceled":[""],"CiStatusLabel|created":[""],"CiStatusLabel|failed":[""],"CiStatusLabel|manual action":[""],"CiStatusLabel|passed":[""],"CiStatusLabel|passed with warnings":[""],"CiStatusLabel|pending":[""],"CiStatusLabel|skipped":[""],"CiStatusLabel|waiting for manual action":[""],"CiStatusText|blocked":[""],"CiStatusText|canceled":[""],"CiStatusText|created":[""],"CiStatusText|failed":[""],"CiStatusText|manual":[""],"CiStatusText|passed":[""],"CiStatusText|pending":[""],"CiStatusText|skipped":[""],"CiStatus|running":[""],"Commit":["",""],"Commit message":[""],"CommitBoxTitle|Commit":[""],"CommitMessage|Add %{file_name}":[""],"Commits":[""],"Commits|History":[""],"Committed by":[""],"Compare":[""],"Contribution guide":[""],"Contributors":[""],"Copy URL to clipboard":[""],"Copy commit SHA to clipboard":[""],"Create New Directory":[""],"Create directory":[""],"Create empty bare repository":[""],"Create merge request":[""],"Create new...":[""],"CreateNewFork|Fork":[""],"CreateTag|Tag":[""],"Cron Timezone":[""],"Cron syntax":[""],"Custom notification events":[""],"Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}.":[""],"Cycle Analytics":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":[""],"CycleAnalyticsStage|Code":[""],"CycleAnalyticsStage|Issue":[""],"CycleAnalyticsStage|Plan":[""],"CycleAnalyticsStage|Production":[""],"CycleAnalyticsStage|Review":[""],"CycleAnalyticsStage|Staging":[""],"CycleAnalyticsStage|Test":[""],"Define a custom pattern with cron syntax":[""],"Delete":[""],"Deploy":["",""],"Description":[""],"Directory name":[""],"Don't show again":[""],"Download":[""],"Download tar":[""],"Download tar.bz2":[""],"Download tar.gz":[""],"Download zip":[""],"DownloadArtifacts|Download":[""],"DownloadCommit|Email Patches":[""],"DownloadCommit|Plain Diff":[""],"DownloadSource|Download":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Every day (at 4:00am)":[""],"Every month (on the 1st at 4:00am)":[""],"Every week (Sundays at 4:00am)":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Files":[""],"Find by path":[""],"Find file":[""],"FirstPushedBy|First":[""],"FirstPushedBy|pushed by":[""],"Fork":["",""],"ForkedFromProjectPath|Forked from":[""],"From issue creation until deploy to production":[""],"From merge request merge until deploy to production":[""],"Go to your fork":[""],"GoToYourFork|Fork":[""],"Home":[""],"Housekeeping successfully started":[""],"Import repository":[""],"Interval Pattern":[""],"Introducing Cycle Analytics":[""],"LFSStatus|Disabled":[""],"LFSStatus|Enabled":[""],"Last %d day":["",""],"Last Pipeline":[""],"Last Update":[""],"Last commit":[""],"Learn more in the":[""],"Learn more in the|pipeline schedules documentation":[""],"Leave group":[""],"Leave project":[""],"Limited to showing %d event at most":["",""],"Median":[""],"MissingSSHKeyWarningLink|add an SSH key":[""],"New Issue":["",""],"New Pipeline Schedule":[""],"New branch":[""],"New directory":[""],"New file":[""],"New issue":[""],"New merge request":[""],"New schedule":[""],"New snippet":[""],"New tag":[""],"No repository":[""],"No schedules":[""],"Not available":[""],"Not enough data":[""],"Notification events":[""],"NotificationEvent|Close issue":[""],"NotificationEvent|Close merge request":[""],"NotificationEvent|Failed pipeline":[""],"NotificationEvent|Merge merge request":[""],"NotificationEvent|New issue":[""],"NotificationEvent|New merge request":[""],"NotificationEvent|New note":[""],"NotificationEvent|Reassign issue":[""],"NotificationEvent|Reassign merge request":[""],"NotificationEvent|Reopen issue":[""],"NotificationEvent|Successful pipeline":[""],"NotificationLevel|Custom":[""],"NotificationLevel|Disabled":[""],"NotificationLevel|Global":[""],"NotificationLevel|On mention":[""],"NotificationLevel|Participate":[""],"NotificationLevel|Watch":[""],"OfSearchInADropdown|Filter":[""],"OpenedNDaysAgo|Opened":[""],"Options":[""],"Owner":[""],"Pipeline":[""],"Pipeline Health":[""],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"PipelineSheduleIntervalPattern|Custom":[""],"Pipeline|with stage":[""],"Pipeline|with stages":[""],"Project '%{project_name}' queued for deletion.":[""],"Project '%{project_name}' was successfully created.":[""],"Project '%{project_name}' was successfully updated.":[""],"Project '%{project_name}' will be deleted.":[""],"Project access must be granted explicitly to each user.":[""],"Project export could not be deleted.":[""],"Project export has been deleted.":[""],"Project export link has expired. Please generate a new export from your project settings.":[""],"Project export started. A download link will be sent by email.":[""],"Project home":[""],"ProjectFeature|Disabled":[""],"ProjectFeature|Everyone with access":[""],"ProjectFeature|Only team members":[""],"ProjectFileTree|Name":[""],"ProjectLastActivity|Never":[""],"ProjectLifecycle|Stage":[""],"ProjectNetworkGraph|Graph":[""],"Read more":[""],"Readme":[""],"RefSwitcher|Branches":[""],"RefSwitcher|Tags":[""],"Related Commits":[""],"Related Deployed Jobs":[""],"Related Issues":[""],"Related Jobs":[""],"Related Merge Requests":[""],"Related Merged Requests":[""],"Remind later":[""],"Remove project":[""],"Request Access":[""],"Revert this commit":[""],"Revert this merge request":[""],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Scheduling Pipelines":[""],"Search branches and tags":[""],"Select Archive Format":[""],"Select a timezone":[""],"Select target branch":[""],"Set a password on your account to pull or push via %{protocol}":[""],"Set up CI":[""],"Set up Koding":[""],"Set up auto deploy":[""],"SetPasswordToCloneLink|set a password":[""],"Showing %d event":["",""],"Source code":[""],"StarProject|Star":[""],"Start a %{new_merge_request} with these changes":[""],"Start a <strong>new merge request</strong> with these changes":[""],"Switch branch/tag":[""],"Tag":["",""],"Tags":[""],"Target Branch":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":[""],"The collection of events added to the data gathered for that stage.":[""],"The fork relationship has been removed.":[""],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":[""],"The phase of the development lifecycle.":[""],"The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.":[""],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":[""],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":[""],"The project can be accessed by any logged in user.":[""],"The project can be accessed without any authentication.":[""],"The repository for this project does not exist.":[""],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":[""],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":[""],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":[""],"The time taken by each data entry gathered by that stage.":[""],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":[""],"This means you can not push code until you create an empty repository or import existing one.":[""],"Time before an issue gets scheduled":[""],"Time before an issue starts implementation":[""],"Time between merge request creation and merge/close":[""],"Time until first merge request":[""],"Timeago|%s days ago":[""],"Timeago|%s days remaining":[""],"Timeago|%s hours remaining":[""],"Timeago|%s minutes ago":[""],"Timeago|%s minutes remaining":[""],"Timeago|%s months ago":[""],"Timeago|%s months remaining":[""],"Timeago|%s seconds remaining":[""],"Timeago|%s weeks ago":[""],"Timeago|%s weeks remaining":[""],"Timeago|%s years ago":[""],"Timeago|%s years remaining":[""],"Timeago|1 day remaining":[""],"Timeago|1 hour remaining":[""],"Timeago|1 minute remaining":[""],"Timeago|1 month remaining":[""],"Timeago|1 week remaining":[""],"Timeago|1 year remaining":[""],"Timeago|Past due":[""],"Timeago|a day ago":[""],"Timeago|a month ago":[""],"Timeago|a week ago":[""],"Timeago|a while":[""],"Timeago|a year ago":[""],"Timeago|about %s hours ago":[""],"Timeago|about a minute ago":[""],"Timeago|about an hour ago":[""],"Timeago|in %s days":[""],"Timeago|in %s hours":[""],"Timeago|in %s minutes":[""],"Timeago|in %s months":[""],"Timeago|in %s seconds":[""],"Timeago|in %s weeks":[""],"Timeago|in %s years":[""],"Timeago|in 1 day":[""],"Timeago|in 1 hour":[""],"Timeago|in 1 minute":[""],"Timeago|in 1 month":[""],"Timeago|in 1 week":[""],"Timeago|in 1 year":[""],"Timeago|less than a minute ago":[""],"Time|hr":["",""],"Time|min":["",""],"Time|s":[""],"Total Time":[""],"Total test time for all commits/merges":[""],"Unstar":[""],"Upload New File":[""],"Upload file":[""],"Use your global notification setting":[""],"VisibilityLevel|Internal":[""],"VisibilityLevel|Private":[""],"VisibilityLevel|Public":[""],"Want to see the data? Please ask an administrator for access.":[""],"We don't have enough data to show this stage.":[""],"Withdraw Access Request":[""],"You are going to remove %{project_name_with_namespace}.\\nRemoved project CANNOT be restored!\\nAre you ABSOLUTELY sure?":[""],"You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?":[""],"You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?":[""],"You can only add files when you are on a branch":[""],"You must sign in to star a project":[""],"You need permission.":[""],"You will not get any notifications via email":[""],"You will only receive notifications for the events you choose":[""],"You will only receive notifications for threads you have participated in":[""],"You will receive notifications for any activity":[""],"You will receive notifications only for comments in which you were @mentioned":[""],"You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account":[""],"You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile":[""],"Your name":[""],"day":["",""],"new merge request":[""],"notification emails":[""],"parent":["",""]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/es/app.js b/app/assets/javascripts/locale/es/app.js index 6977625f4d8..eafcd15acf9 100644 --- a/app/assets/javascripts/locale/es/app.js +++ b/app/assets/javascripts/locale/es/app.js @@ -1 +1 @@ -var locales = locales || {}; locales['es'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-06-07 12:29-0500","Language-Team":"Spanish","Language":"es","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"Bob Van Landuyt <bob@gitlab.com>","X-Generator":"Poedit 2.0.2","lang":"es","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"About auto deploy":["Acerca del auto despliegue"],"Activity":["Actividad"],"Add Changelog":["Agregar Changelog"],"Add Contribution guide":["Agregar guía de contribución"],"Add License":["Agregar Licencia"],"Add an SSH key to your profile to pull or push via SSH.":["Agregar una clave SSH a tu perfil para actualizar o enviar a través de SSH."],"Add new directory":["Agregar nuevo directorio"],"Archived project! Repository is read-only":["¡Proyecto archivado! El repositorio es de sólo lectura"],"Branch":["Rama","Ramas"],"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}":["La rama <strong>%{branch_name}</strong> fue creada. Para configurar el auto despliegue, escoge una plantilla Yaml para GitLab CI y envía tus cambios. %{link_to_autodeploy_doc}"],"Branches":["Ramas"],"ByAuthor|by":["por"],"CI configuration":["Configuración de CI"],"Changelog":["Changelog"],"Charts":["Gráficos"],"CiStatusLabel|canceled":["cancelado"],"CiStatusLabel|created":["creado"],"CiStatusLabel|failed":["fallado"],"CiStatusLabel|manual action":["acción manual"],"CiStatusLabel|passed":["pasó"],"CiStatusLabel|passed with warnings":["pasó con advertencias"],"CiStatusLabel|pending":["pendiente"],"CiStatusLabel|skipped":["omitido"],"CiStatusLabel|waiting for manual action":["esperando acción manual"],"CiStatusText|blocked":["bloqueado"],"CiStatusText|canceled":["cancelado"],"CiStatusText|created":["creado"],"CiStatusText|failed":["fallado"],"CiStatusText|manual":["manual"],"CiStatusText|passed":["pasó"],"CiStatusText|pending":["pendiente"],"CiStatusText|skipped":["omitido"],"CiStatus|running":["en ejecución"],"Commit":["Cambio","Cambios"],"CommitMessage|Add %{file_name}":["Agregar %{file_name}"],"Commits":["Cambios"],"Commits|History":["Historial"],"Compare":["Comparar"],"Contribution guide":["Guía de contribución"],"Contributors":["Contribuidores"],"Copy URL to clipboard":["Copiar URL al portapapeles"],"Copy commit SHA to clipboard":["Copiar SHA del cambio al portapapeles"],"Create New Directory":["Crear Nuevo Directorio"],"Create directory":["Crear directorio"],"Create empty bare repository":["Crear repositorio vacío"],"Create merge request":["Crear solicitud de fusión"],"CreateNewFork|Fork":["Bifurcar"],"Custom notification events":["Eventos de notificaciones personalizadas"],"Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}.":["Los niveles de notificación personalizados son los mismos que los niveles participantes. Con los niveles de notificación personalizados, también recibirá notificaciones para eventos seleccionados. Para obtener más información, consulte %{notification_link}."],"Cycle Analytics":["Cycle Analytics"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Cycle Analytics ofrece una visión general de cuánto tiempo tarda en pasar de idea a producción en su proyecto."],"CycleAnalyticsStage|Code":["Código"],"CycleAnalyticsStage|Issue":["Incidencia"],"CycleAnalyticsStage|Plan":["Planificación"],"CycleAnalyticsStage|Production":["Producción"],"CycleAnalyticsStage|Review":["Revisión"],"CycleAnalyticsStage|Staging":["Puesta en escena"],"CycleAnalyticsStage|Test":["Pruebas"],"Deploy":["Despliegue","Despliegues"],"Directory name":["Nombre del directorio"],"Don't show again":["No mostrar de nuevo"],"Download tar":["Descargar tar"],"Download tar.bz2":["Descargar tar.bz2"],"Download tar.gz":["Descargar tar.gz"],"Download zip":["Descargar zip"],"DownloadArtifacts|Download":["Descargar"],"DownloadSource|Download":["Descargar"],"Files":["Archivos"],"Find by path":["Buscar por ruta"],"Find file":["Buscar archivo"],"FirstPushedBy|First":["Primer"],"FirstPushedBy|pushed by":["enviado por"],"ForkedFromProjectPath|Forked from":["Bifurcado de"],"Forks":["Bifurcaciones"],"From issue creation until deploy to production":["Desde la creación de la incidencia hasta el despliegue a producción"],"From merge request merge until deploy to production":["Desde la integración de la solicitud de fusión hasta el despliegue a producción"],"Go to your fork":["Ir a tu bifurcación"],"GoToYourFork|Fork":["Bifurcación"],"Home":["Inicio"],"Housekeeping successfully started":["Servicio de limpieza iniciado con éxito"],"Import repository":["Importar repositorio"],"Introducing Cycle Analytics":["Introducción a Cycle Analytics"],"LFSStatus|Disabled":["Deshabilitado"],"LFSStatus|Enabled":["Habilitado"],"Last %d day":["Último %d día","Últimos %d días"],"Last Update":["Última actualización"],"Last commit":["Último cambio"],"Leave group":["Abandonar grupo"],"Leave project":["Abandonar proyecto"],"Limited to showing %d event at most":["Limitado a mostrar máximo %d evento","Limitado a mostrar máximo %d eventos"],"Median":["Mediana"],"MissingSSHKeyWarningLink|add an SSH key":["agregar una clave SSH"],"New Issue":["Nueva incidencia","Nuevas incidencias"],"New branch":["Nueva rama"],"New directory":["Nuevo directorio"],"New file":["Nuevo archivo"],"New issue":["Nueva incidencia"],"New merge request":["Nueva solicitud de fusión"],"New snippet":["Nuevo fragmento de código"],"New tag":["Nueva etiqueta"],"No repository":["No hay repositorio"],"Not available":["No disponible"],"Not enough data":["No hay suficientes datos"],"Notification events":["Eventos de notificación"],"NotificationEvent|Close issue":["Cerrar incidencia"],"NotificationEvent|Close merge request":["Cerrar solicitud de fusión"],"NotificationEvent|Failed pipeline":["Pipeline fallido"],"NotificationEvent|Merge merge request":["Integrar solicitud de fusión"],"NotificationEvent|New issue":["Nueva incidencia"],"NotificationEvent|New merge request":["Nueva solicitud de fusión"],"NotificationEvent|New note":["Nueva nota"],"NotificationEvent|Reassign issue":["Reasignar incidencia"],"NotificationEvent|Reassign merge request":["Reasignar solicitud de fusión"],"NotificationEvent|Reopen issue":["Reabrir incidencia"],"NotificationEvent|Successful pipeline":["Pipeline exitoso"],"NotificationLevel|Custom":["Personalizado"],"NotificationLevel|Disabled":["Deshabilitado"],"NotificationLevel|Global":["Global"],"NotificationLevel|On mention":["Cuando me mencionan"],"NotificationLevel|Participate":["Participación"],"NotificationLevel|Watch":["Vigilancia"],"OpenedNDaysAgo|Opened":["Abierto"],"Pipeline Health":["Estado del Pipeline"],"Project '%{project_name}' queued for deletion.":["Proyecto ‘%{project_name}’ en cola para eliminación."],"Project '%{project_name}' was successfully created.":["Proyecto ‘%{project_name}’ fue creado satisfactoriamente."],"Project '%{project_name}' was successfully updated.":["Proyecto ‘%{project_name}’ fue actualizado satisfactoriamente."],"Project '%{project_name}' will be deleted.":["Proyecto ‘%{project_name}’ será eliminado."],"Project access must be granted explicitly to each user.":["El acceso al proyecto debe concederse explícitamente a cada usuario."],"Project export could not be deleted.":["No se pudo eliminar la exportación del proyecto."],"Project export has been deleted.":["La exportación del proyecto ha sido eliminada."],"Project export link has expired. Please generate a new export from your project settings.":["El enlace de exportación del proyecto ha caducado. Por favor, genera una nueva exportación desde la configuración del proyecto."],"Project export started. A download link will be sent by email.":["Se inició la exportación del proyecto. Se enviará un enlace de descarga por correo electrónico."],"Project home":["Inicio del proyecto"],"ProjectFeature|Disabled":["Deshabilitada"],"ProjectFeature|Everyone with access":["Todos con acceso"],"ProjectFeature|Only team members":["Solo miembros del equipo"],"ProjectFileTree|Name":["Nombre"],"ProjectLastActivity|Never":["Nunca"],"ProjectLifecycle|Stage":["Etapa"],"ProjectNetworkGraph|Graph":["Historial gráfico"],"Read more":["Leer más"],"Readme":["Readme"],"RefSwitcher|Branches":["Ramas"],"RefSwitcher|Tags":["Etiquetas"],"Related Commits":["Cambios Relacionados"],"Related Deployed Jobs":["Trabajos Desplegados Relacionados"],"Related Issues":["Incidencias Relacionadas"],"Related Jobs":["Trabajos Relacionados"],"Related Merge Requests":["Solicitudes de fusión Relacionadas"],"Related Merged Requests":["Solicitudes de fusión Relacionadas"],"Remind later":["Recordar después"],"Remove project":["Eliminar proyecto"],"Request Access":["Solicitar acceso"],"Search branches and tags":["Buscar ramas y etiquetas"],"Select Archive Format":["Seleccionar formato de archivo"],"Set a password on your account to pull or push via %{protocol}":["Establezca una contraseña en su cuenta para actualizar o enviar a través de% {protocol}"],"Set up CI":["Configurar CI"],"Set up Koding":["Configurar Koding"],"Set up auto deploy":["Configurar auto despliegue"],"SetPasswordToCloneLink|set a password":["establecer una contraseña"],"Showing %d event":["Mostrando %d evento","Mostrando %d eventos"],"Source code":["Código fuente"],"StarProject|Star":["Destacar"],"Switch branch/tag":["Cambiar rama/etiqueta"],"Tag":["Etiqueta","Etiquetas"],"Tags":["Etiquetas"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["La etapa de desarrollo muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquí una vez creada tu primera solicitud de fusión."],"The collection of events added to the data gathered for that stage.":["La colección de eventos agregados a los datos recopilados para esa etapa."],"The fork relationship has been removed.":["La relación con la bifurcación se ha eliminado."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["La etapa de incidencia muestra el tiempo que toma desde la creación de un tema hasta asignar el tema a un hito, o añadir el tema a una lista en el panel de temas. Empieza a crear temas para ver los datos de esta etapa."],"The phase of the development lifecycle.":["La etapa del ciclo de vida de desarrollo."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["La etapa de planificación muestra el tiempo desde el paso anterior hasta el envío de tu primera confirmación. Este tiempo se añadirá automáticamente una vez que usted envíe el primer cambio."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["La etapa de producción muestra el tiempo total que tarda entre la creación de una incidencia y el despliegue del código a producción. Los datos se añadirán automáticamente una vez haya finalizado por completo el ciclo de idea a producción."],"The project can be accessed by any logged in user.":["El proyecto puede ser accedido por cualquier usuario conectado."],"The project can be accessed without any authentication.":["El proyecto puede accederse sin ninguna autenticación."],"The repository for this project does not exist.":["El repositorio para este proyecto no existe."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["La etapa de revisión muestra el tiempo desde la creación de la solicitud de fusión hasta que los cambios se fusionaron. Los datos se añadirán automáticamente después de fusionar su primera solicitud de fusión."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["La etapa de puesta en escena muestra el tiempo entre la fusión y el despliegue de código en el entorno de producción. Los datos se añadirán automáticamente una vez que se despliega a producción por primera vez."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["La etapa de pruebas muestra el tiempo que GitLab CI toma para ejecutar cada pipeline para la solicitud de fusión relacionada. Los datos se añadirán automáticamente luego de que el primer pipeline termine de ejecutarse."],"The time taken by each data entry gathered by that stage.":["El tiempo utilizado por cada entrada de datos obtenido por esa etapa."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["El valor en el punto medio de una serie de valores observados. Por ejemplo, entre 3, 5, 9, la mediana es 5. Entre 3, 5, 7, 8, la mediana es (5 + 7) / 2 = 6."],"This means you can not push code until you create an empty repository or import existing one.":["Esto significa que no puede enviar código hasta que cree un repositorio vacío o importe uno existente."],"Time before an issue gets scheduled":["Tiempo antes de que una incidencia sea programada"],"Time before an issue starts implementation":["Tiempo antes de que empieze la implementación de una incidencia"],"Time between merge request creation and merge/close":["Tiempo entre la creación de la solicitud de fusión y la integración o cierre de ésta"],"Time until first merge request":["Tiempo hasta la primera solicitud de fusión"],"Timeago|%s days ago":["hace %s días"],"Timeago|%s days remaining":["%s días restantes"],"Timeago|%s hours remaining":["%s horas restantes"],"Timeago|%s minutes ago":["hace %s minutos"],"Timeago|%s minutes remaining":["%s minutos restantes"],"Timeago|%s months ago":["hace %s meses"],"Timeago|%s months remaining":["%s meses restantes"],"Timeago|%s seconds remaining":["%s segundos restantes"],"Timeago|%s weeks ago":["hace %s semanas"],"Timeago|%s weeks remaining":["%s semanas restantes"],"Timeago|%s years ago":["hace %s años"],"Timeago|%s years remaining":["%s años restantes"],"Timeago|1 day remaining":["1 día restante"],"Timeago|1 hour remaining":["1 hora restante"],"Timeago|1 minute remaining":["1 minuto restante"],"Timeago|1 month remaining":["1 mes restante"],"Timeago|1 week remaining":["1 semana restante"],"Timeago|1 year remaining":["1 año restante"],"Timeago|Past due":["Atrasado"],"Timeago|a day ago":["hace un día"],"Timeago|a month ago":["hace 1 mes"],"Timeago|a week ago":["hace 1 semana"],"Timeago|a while":["hace un momento"],"Timeago|a year ago":["hace 1 año"],"Timeago|about %s hours ago":["hace alrededor de %s horas"],"Timeago|about a minute ago":["hace alrededor de 1 minuto"],"Timeago|about an hour ago":["hace alrededor de 1 hora"],"Timeago|in %s days":["en %s días"],"Timeago|in %s hours":["en %s horas"],"Timeago|in %s minutes":["en %s minutos"],"Timeago|in %s months":["en %s meses"],"Timeago|in %s seconds":["en %s segundos"],"Timeago|in %s weeks":["en %s semanas"],"Timeago|in %s years":["en %s años"],"Timeago|in 1 day":["en 1 día"],"Timeago|in 1 hour":["en 1 hora"],"Timeago|in 1 minute":["en 1 minuto"],"Timeago|in 1 month":["en 1 mes"],"Timeago|in 1 week":["en 1 semana"],"Timeago|in 1 year":["en 1 año"],"Timeago|less than a minute ago":["hace menos de 1 minuto"],"Time|hr":["hr","hrs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Tiempo Total"],"Total test time for all commits/merges":["Tiempo total de pruebas para todos los cambios o integraciones"],"Unstar":["No Destacar"],"Upload New File":["Subir nuevo archivo"],"Upload file":["Subir archivo"],"Use your global notification setting":["Utiliza tu configuración de notificación global"],"VisibilityLevel|Internal":["Interno"],"VisibilityLevel|Private":["Privado"],"VisibilityLevel|Public":["Público"],"Want to see the data? Please ask an administrator for access.":["¿Quieres ver los datos? Por favor pide acceso al administrador."],"We don't have enough data to show this stage.":["No hay suficientes datos para mostrar en esta etapa."],"Withdraw Access Request":["Retirar Solicitud de Acceso"],"You are going to remove %{project_name_with_namespace}.\\nRemoved project CANNOT be restored!\\nAre you ABSOLUTELY sure?":["Va a eliminar %{project_name_with_namespace}.\\n¡El proyecto eliminado NO puede ser restaurado!\\n¿Estás TOTALMENTE seguro?"],"You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?":["Vas a eliminar el enlace de la bifurcación con el proyecto original %{forked_from_project}. ¿Estás TOTALMENTE seguro?"],"You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?":["Vas a transferir %{project_name_with_namespace} a otro propietario. ¿Estás TOTALMENTE seguro?"],"You can only add files when you are on a branch":["Sólo puede agregar archivos cuando estas en una rama"],"You must sign in to star a project":["Debes iniciar sesión para destacar un proyecto"],"You need permission.":["Necesitas permisos."],"You will not get any notifications via email":["No recibirás ninguna notificación por correo electrónico"],"You will only receive notifications for the events you choose":["Solo recibirás notificaciones de los eventos que elijas"],"You will only receive notifications for threads you have participated in":["Solo recibirás notificaciones de los temas en los que has participado"],"You will receive notifications for any activity":["Recibirás notificaciones para cualquier actividad"],"You will receive notifications only for comments in which you were @mentioned":["Recibirás notificaciones sólo para los comentarios en los que se te mencionó"],"You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account":["No podrás actualizar o enviar código al proyecto a través de %{protocol} hasta que %{set_password_link} en tu cuenta"],"You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile":["No podrás actualizar o enviar código al proyecto a través de SSH hasta que %{add_ssh_key_link} en su perfil"],"Your name":["Tu nombre"],"committed":["cambió"],"day":["día","días"],"notification emails":["correos electrónicos de notificación"]}}};
\ No newline at end of file +var locales = locales || {}; locales['es'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-06-15 21:59-0500","Language-Team":"Spanish","Language":"es","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"Bob Van Landuyt <bob@gitlab.com>","X-Generator":"Poedit 2.0.2","POT-Creation-Date":"2017-06-15 21:59-0500","lang":"es","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"%{commit_author_link} committed %{commit_timeago}":["%{commit_author_link} cambió %{commit_timeago}"],"About auto deploy":["Acerca del auto despliegue"],"Active":["Activo"],"Activity":["Actividad"],"Add Changelog":["Agregar Changelog"],"Add Contribution guide":["Agregar guía de contribución"],"Add License":["Agregar Licencia"],"Add an SSH key to your profile to pull or push via SSH.":["Agregar una clave SSH a tu perfil para actualizar o enviar a través de SSH."],"Add new directory":["Agregar nuevo directorio"],"Archived project! Repository is read-only":["¡Proyecto archivado! El repositorio es de solo lectura"],"Are you sure you want to delete this pipeline schedule?":["¿Estás seguro que deseas eliminar esta programación del pipeline?"],"Attach a file by drag & drop or %{upload_link}":["Adjunte un archivo arrastrando & soltando o %{upload_link}"],"Branch":["Rama","Ramas"],"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}":["La rama <strong>%{branch_name}</strong> fue creada. Para configurar el auto despliegue, escoge una plantilla Yaml para GitLab CI y envía tus cambios. %{link_to_autodeploy_doc}"],"Branches":["Ramas"],"Browse files":["Examinar los archivos"],"ByAuthor|by":["por"],"CI configuration":["Configuración de CI"],"Cancel":["Cancelar"],"ChangeTypeActionLabel|Pick into branch":["Escoger en la rama"],"ChangeTypeActionLabel|Revert in branch":["Revertir en la rama"],"ChangeTypeAction|Cherry-pick":["Cherry-pick"],"ChangeTypeAction|Revert":["Revertir"],"Changelog":["Changelog"],"Charts":["Gráficos"],"Cherry-pick this commit":["Escoger este cambio"],"Cherry-pick this merge request":["Escoger esta solicitud de fusión"],"CiStatusLabel|canceled":["cancelado"],"CiStatusLabel|created":["creado"],"CiStatusLabel|failed":["fallido"],"CiStatusLabel|manual action":["acción manual"],"CiStatusLabel|passed":["pasó"],"CiStatusLabel|passed with warnings":["pasó con advertencias"],"CiStatusLabel|pending":["pendiente"],"CiStatusLabel|skipped":["omitido"],"CiStatusLabel|waiting for manual action":["esperando acción manual"],"CiStatusText|blocked":["bloqueado"],"CiStatusText|canceled":["cancelado"],"CiStatusText|created":["creado"],"CiStatusText|failed":["fallado"],"CiStatusText|manual":["manual"],"CiStatusText|passed":["pasó"],"CiStatusText|pending":["pendiente"],"CiStatusText|skipped":["omitido"],"CiStatus|running":["en ejecución"],"Commit":["Cambio","Cambios"],"Commit message":["Mensaje del cambio"],"CommitBoxTitle|Commit":["Cambio"],"CommitMessage|Add %{file_name}":["Agregar %{file_name}"],"Commits":["Cambios"],"Commits|History":["Historial"],"Committed by":["Enviado por"],"Compare":["Comparar"],"Contribution guide":["Guía de contribución"],"Contributors":["Contribuidores"],"Copy URL to clipboard":["Copiar URL al portapapeles"],"Copy commit SHA to clipboard":["Copiar SHA del cambio al portapapeles"],"Create New Directory":["Crear Nuevo Directorio"],"Create directory":["Crear directorio"],"Create empty bare repository":["Crear repositorio vacío"],"Create merge request":["Crear solicitud de fusión"],"Create new...":["Crear nuevo..."],"CreateNewFork|Fork":["Bifurcar"],"CreateTag|Tag":["Etiqueta"],"Cron Timezone":["Zona horaria del Cron"],"Cron syntax":["Sintaxis de Cron"],"Custom notification events":["Eventos de notificaciones personalizadas"],"Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}.":["Los niveles de notificación personalizados son los mismos que los niveles participantes. Con los niveles de notificación personalizados, también recibirá notificaciones para eventos seleccionados. Para obtener más información, consulte %{notification_link}."],"Cycle Analytics":["Cycle Analytics"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Cycle Analytics ofrece una visión general de cuánto tiempo tarda en pasar de idea a producción en su proyecto."],"CycleAnalyticsStage|Code":["Código"],"CycleAnalyticsStage|Issue":["Incidencia"],"CycleAnalyticsStage|Plan":["Planificación"],"CycleAnalyticsStage|Production":["Producción"],"CycleAnalyticsStage|Review":["Revisión"],"CycleAnalyticsStage|Staging":["Puesta en escena"],"CycleAnalyticsStage|Test":["Pruebas"],"Define a custom pattern with cron syntax":["Definir un patrón personalizado con la sintaxis de cron"],"Delete":["Eliminar"],"Deploy":["Despliegue","Despliegues"],"Description":["Descripción"],"Directory name":["Nombre del directorio"],"Don't show again":["No mostrar de nuevo"],"Download":["Descargar"],"Download tar":["Descargar tar"],"Download tar.bz2":["Descargar tar.bz2"],"Download tar.gz":["Descargar tar.gz"],"Download zip":["Descargar zip"],"DownloadArtifacts|Download":["Descargar"],"DownloadCommit|Email Patches":["Parches por correo electrónico"],"DownloadCommit|Plain Diff":["Diferencias en texto plano"],"DownloadSource|Download":["Descargar"],"Edit":["Editar"],"Edit Pipeline Schedule %{id}":["Editar Programación del Pipeline %{id}"],"Every day (at 4:00am)":["Todos los días (a las 4:00 am)"],"Every month (on the 1st at 4:00am)":["Todos los meses (el día 1 a las 4:00 am)"],"Every week (Sundays at 4:00am)":["Todas las semanas (domingos a las 4:00 am)"],"Failed to change the owner":["Error al cambiar el propietario"],"Failed to remove the pipeline schedule":["Error al eliminar la programación del pipeline"],"Files":["Archivos"],"Find by path":["Buscar por ruta"],"Find file":["Buscar archivo"],"FirstPushedBy|First":["Primer"],"FirstPushedBy|pushed by":["enviado por"],"Fork":["Bifurcación","Bifurcaciones"],"ForkedFromProjectPath|Forked from":["Bifurcado de"],"From issue creation until deploy to production":["Desde la creación de la incidencia hasta el despliegue a producción"],"From merge request merge until deploy to production":["Desde la integración de la solicitud de fusión hasta el despliegue a producción"],"Go to your fork":["Ir a tu bifurcación"],"GoToYourFork|Fork":["Bifurcación"],"Home":["Inicio"],"Housekeeping successfully started":["Servicio de limpieza iniciado con éxito"],"Import repository":["Importar repositorio"],"Interval Pattern":["Patrón de intervalo"],"Introducing Cycle Analytics":["Introducción a Cycle Analytics"],"LFSStatus|Disabled":["Deshabilitado"],"LFSStatus|Enabled":["Habilitado"],"Last %d day":["Último %d día","Últimos %d días"],"Last Pipeline":["Último Pipeline"],"Last Update":["Última actualización"],"Last commit":["Último cambio"],"Learn more in the":["Más información en la"],"Learn more in the|pipeline schedules documentation":["documentación sobre la programación de pipelines"],"Leave group":["Abandonar grupo"],"Leave project":["Abandonar proyecto"],"Limited to showing %d event at most":["Limitado a mostrar máximo %d evento","Limitado a mostrar máximo %d eventos"],"Median":["Mediana"],"MissingSSHKeyWarningLink|add an SSH key":["agregar una clave SSH"],"New Issue":["Nueva incidencia","Nuevas incidencias"],"New Pipeline Schedule":["Nueva Programación del Pipeline"],"New branch":["Nueva rama"],"New directory":["Nuevo directorio"],"New file":["Nuevo archivo"],"New issue":["Nueva incidencia"],"New merge request":["Nueva solicitud de fusión"],"New schedule":["Nueva programación"],"New snippet":["Nuevo fragmento de código"],"New tag":["Nueva etiqueta"],"No repository":["No hay repositorio"],"No schedules":["No hay programaciones"],"Not available":["No disponible"],"Not enough data":["No hay suficientes datos"],"Notification events":["Eventos de notificación"],"NotificationEvent|Close issue":["Cerrar incidencia"],"NotificationEvent|Close merge request":["Cerrar solicitud de fusión"],"NotificationEvent|Failed pipeline":["Pipeline fallido"],"NotificationEvent|Merge merge request":["Integrar solicitud de fusión"],"NotificationEvent|New issue":["Nueva incidencia"],"NotificationEvent|New merge request":["Nueva solicitud de fusión"],"NotificationEvent|New note":["Nueva nota"],"NotificationEvent|Reassign issue":["Reasignar incidencia"],"NotificationEvent|Reassign merge request":["Reasignar solicitud de fusión"],"NotificationEvent|Reopen issue":["Reabrir incidencia"],"NotificationEvent|Successful pipeline":["Pipeline exitoso"],"NotificationLevel|Custom":["Personalizado"],"NotificationLevel|Disabled":["Deshabilitado"],"NotificationLevel|Global":["Global"],"NotificationLevel|On mention":["Cuando me mencionan"],"NotificationLevel|Participate":["Participación"],"NotificationLevel|Watch":["Vigilancia"],"OfSearchInADropdown|Filter":["Filtrar"],"OpenedNDaysAgo|Opened":["Abierto"],"Options":["Opciones"],"Owner":["Propietario"],"Pipeline":["Pipeline"],"Pipeline Health":["Estado del Pipeline"],"Pipeline Schedule":["Programación del Pipeline"],"Pipeline Schedules":["Programaciones de los Pipelines"],"PipelineSchedules|Activated":["Activado"],"PipelineSchedules|Active":["Activos"],"PipelineSchedules|All":["Todos"],"PipelineSchedules|Inactive":["Inactivos"],"PipelineSchedules|Next Run":["Próxima Ejecución"],"PipelineSchedules|None":["Ninguno"],"PipelineSchedules|Provide a short description for this pipeline":["Proporcione una breve descripción para este pipeline"],"PipelineSchedules|Take ownership":["Tomar posesión"],"PipelineSchedules|Target":["Destino"],"PipelineSheduleIntervalPattern|Custom":["Personalizado"],"Pipeline|with stage":["con etapa"],"Pipeline|with stages":["con etapas"],"Project '%{project_name}' queued for deletion.":["Proyecto ‘%{project_name}’ en cola para eliminación."],"Project '%{project_name}' was successfully created.":["Proyecto ‘%{project_name}’ fue creado satisfactoriamente."],"Project '%{project_name}' was successfully updated.":["Proyecto ‘%{project_name}’ fue actualizado satisfactoriamente."],"Project '%{project_name}' will be deleted.":["Proyecto ‘%{project_name}’ será eliminado."],"Project access must be granted explicitly to each user.":["El acceso al proyecto debe concederse explícitamente a cada usuario."],"Project export could not be deleted.":["No se pudo eliminar la exportación del proyecto."],"Project export has been deleted.":["La exportación del proyecto ha sido eliminada."],"Project export link has expired. Please generate a new export from your project settings.":["El enlace de exportación del proyecto ha caducado. Por favor, genera una nueva exportación desde la configuración del proyecto."],"Project export started. A download link will be sent by email.":["Se inició la exportación del proyecto. Se enviará un enlace de descarga por correo electrónico."],"Project home":["Inicio del proyecto"],"ProjectFeature|Disabled":["Deshabilitada"],"ProjectFeature|Everyone with access":["Todos con acceso"],"ProjectFeature|Only team members":["Solo miembros del equipo"],"ProjectFileTree|Name":["Nombre"],"ProjectLastActivity|Never":["Nunca"],"ProjectLifecycle|Stage":["Etapa"],"ProjectNetworkGraph|Graph":["Historial gráfico"],"Read more":["Leer más"],"Readme":["Léeme"],"RefSwitcher|Branches":["Ramas"],"RefSwitcher|Tags":["Etiquetas"],"Related Commits":["Cambios Relacionados"],"Related Deployed Jobs":["Trabajos Desplegados Relacionados"],"Related Issues":["Incidencias Relacionadas"],"Related Jobs":["Trabajos Relacionados"],"Related Merge Requests":["Solicitudes de fusión Relacionadas"],"Related Merged Requests":["Solicitudes de fusión Relacionadas"],"Remind later":["Recordar después"],"Remove project":["Eliminar proyecto"],"Request Access":["Solicitar acceso"],"Revert this commit":["Revertir este cambio"],"Revert this merge request":["Revertir esta solicitud de fusión"],"Save pipeline schedule":["Guardar programación del pipeline"],"Schedule a new pipeline":["Programar un nuevo pipeline"],"Scheduling Pipelines":["Programación de Pipelines"],"Search branches and tags":["Buscar ramas y etiquetas"],"Select Archive Format":["Seleccionar formato de archivo"],"Select a timezone":["Selecciona una zona horaria"],"Select target branch":["Selecciona una rama de destino"],"Set a password on your account to pull or push via %{protocol}":["Establezca una contraseña en su cuenta para actualizar o enviar a través de %{protocol}"],"Set up CI":["Configurar CI"],"Set up Koding":["Configurar Koding"],"Set up auto deploy":["Configurar auto despliegue"],"SetPasswordToCloneLink|set a password":["establecer una contraseña"],"Showing %d event":["Mostrando %d evento","Mostrando %d eventos"],"Source code":["Código fuente"],"StarProject|Star":["Destacar"],"Start a %{new_merge_request} with these changes":["Iniciar una %{new_merge_request} con estos cambios"],"Switch branch/tag":["Cambiar rama/etiqueta"],"Tag":["Etiqueta","Etiquetas"],"Tags":["Etiquetas"],"Target Branch":["Rama de destino"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["La etapa de desarrollo muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquí una vez creada tu primera solicitud de fusión."],"The collection of events added to the data gathered for that stage.":["La colección de eventos agregados a los datos recopilados para esa etapa."],"The fork relationship has been removed.":["La relación con la bifurcación se ha eliminado."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["La etapa de incidencia muestra el tiempo que toma desde la creación de un tema hasta asignar el tema a un hito, o añadir el tema a una lista en el panel de temas. Empieza a crear temas para ver los datos de esta etapa."],"The phase of the development lifecycle.":["La etapa del ciclo de vida de desarrollo."],"The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.":["La programación de pipelines ejecuta pipelines en el futuro, repetidamente, para ramas o etiquetas específicas. Los pipelines programados heredarán acceso limitado al proyecto basado en su usuario asociado."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["La etapa de planificación muestra el tiempo desde el paso anterior hasta el envío de tu primera confirmación. Este tiempo se añadirá automáticamente una vez que usted envíe el primer cambio."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["La etapa de producción muestra el tiempo total que tarda entre la creación de una incidencia y el despliegue del código a producción. Los datos se añadirán automáticamente una vez haya finalizado por completo el ciclo de idea a producción."],"The project can be accessed by any logged in user.":["El proyecto puede ser accedido por cualquier usuario conectado."],"The project can be accessed without any authentication.":["El proyecto puede accederse sin ninguna autenticación."],"The repository for this project does not exist.":["El repositorio para este proyecto no existe."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["La etapa de revisión muestra el tiempo desde la creación de la solicitud de fusión hasta que los cambios se fusionaron. Los datos se añadirán automáticamente después de fusionar su primera solicitud de fusión."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["La etapa de puesta en escena muestra el tiempo entre la fusión y el despliegue de código en el entorno de producción. Los datos se añadirán automáticamente una vez que se despliega a producción por primera vez."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["La etapa de pruebas muestra el tiempo que GitLab CI toma para ejecutar cada pipeline para la solicitud de fusión relacionada. Los datos se añadirán automáticamente luego de que el primer pipeline termine de ejecutarse."],"The time taken by each data entry gathered by that stage.":["El tiempo utilizado por cada entrada de datos obtenido por esa etapa."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["El valor en el punto medio de una serie de valores observados. Por ejemplo, entre 3, 5, 9, la mediana es 5. Entre 3, 5, 7, 8, la mediana es (5 + 7) / 2 = 6."],"This means you can not push code until you create an empty repository or import existing one.":["Esto significa que no puede enviar código hasta que cree un repositorio vacío o importe uno existente."],"Time before an issue gets scheduled":["Tiempo antes de que una incidencia sea programada"],"Time before an issue starts implementation":["Tiempo antes de que empieze la implementación de una incidencia"],"Time between merge request creation and merge/close":["Tiempo entre la creación de la solicitud de fusión y la integración o cierre de ésta"],"Time until first merge request":["Tiempo hasta la primera solicitud de fusión"],"Timeago|%s days ago":["hace %s días"],"Timeago|%s days remaining":["%s días restantes"],"Timeago|%s hours remaining":["%s horas restantes"],"Timeago|%s minutes ago":["hace %s minutos"],"Timeago|%s minutes remaining":["%s minutos restantes"],"Timeago|%s months ago":["hace %s meses"],"Timeago|%s months remaining":["%s meses restantes"],"Timeago|%s seconds remaining":["%s segundos restantes"],"Timeago|%s weeks ago":["hace %s semanas"],"Timeago|%s weeks remaining":["%s semanas restantes"],"Timeago|%s years ago":["hace %s años"],"Timeago|%s years remaining":["%s años restantes"],"Timeago|1 day remaining":["1 día restante"],"Timeago|1 hour remaining":["1 hora restante"],"Timeago|1 minute remaining":["1 minuto restante"],"Timeago|1 month remaining":["1 mes restante"],"Timeago|1 week remaining":["1 semana restante"],"Timeago|1 year remaining":["1 año restante"],"Timeago|Past due":["Atrasado"],"Timeago|a day ago":["hace un día"],"Timeago|a month ago":["hace un mes"],"Timeago|a week ago":["hace una semana"],"Timeago|a while":["hace un momento"],"Timeago|a year ago":["hace un año"],"Timeago|about %s hours ago":["hace alrededor de %s horas"],"Timeago|about a minute ago":["hace alrededor de 1 minuto"],"Timeago|about an hour ago":["hace alrededor de 1 hora"],"Timeago|in %s days":["en %s días"],"Timeago|in %s hours":["en %s horas"],"Timeago|in %s minutes":["en %s minutos"],"Timeago|in %s months":["en %s meses"],"Timeago|in %s seconds":["en %s segundos"],"Timeago|in %s weeks":["en %s semanas"],"Timeago|in %s years":["en %s años"],"Timeago|in 1 day":["en 1 día"],"Timeago|in 1 hour":["en 1 hora"],"Timeago|in 1 minute":["en 1 minuto"],"Timeago|in 1 month":["en 1 mes"],"Timeago|in 1 week":["en 1 semana"],"Timeago|in 1 year":["en 1 año"],"Timeago|less than a minute ago":["hace menos de 1 minuto"],"Time|hr":["hr","hrs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Tiempo Total"],"Total test time for all commits/merges":["Tiempo total de pruebas para todos los cambios o integraciones"],"Unstar":["No Destacar"],"Upload New File":["Subir nuevo archivo"],"Upload file":["Subir archivo"],"Use your global notification setting":["Utiliza tu configuración de notificación global"],"VisibilityLevel|Internal":["Interno"],"VisibilityLevel|Private":["Privado"],"VisibilityLevel|Public":["Público"],"Want to see the data? Please ask an administrator for access.":["¿Quieres ver los datos? Por favor pide acceso al administrador."],"We don't have enough data to show this stage.":["No hay suficientes datos para mostrar en esta etapa."],"Withdraw Access Request":["Retirar Solicitud de Acceso"],"You are going to remove %{project_name_with_namespace}.\\nRemoved project CANNOT be restored!\\nAre you ABSOLUTELY sure?":["Va a eliminar %{project_name_with_namespace}.\\n¡El proyecto eliminado NO puede ser restaurado!\\n¿Estás TOTALMENTE seguro?"],"You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?":["Vas a eliminar el enlace de la bifurcación con el proyecto original %{forked_from_project}. ¿Estás TOTALMENTE seguro?"],"You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?":["Vas a transferir %{project_name_with_namespace} a otro propietario. ¿Estás TOTALMENTE seguro?"],"You can only add files when you are on a branch":["Solo puedes agregar archivos cuando estás en una rama"],"You must sign in to star a project":["Debes iniciar sesión para destacar un proyecto"],"You need permission.":["Necesitas permisos."],"You will not get any notifications via email":["No recibirás ninguna notificación por correo electrónico"],"You will only receive notifications for the events you choose":["Solo recibirás notificaciones de los eventos que elijas"],"You will only receive notifications for threads you have participated in":["Solo recibirás notificaciones de los temas en los que has participado"],"You will receive notifications for any activity":["Recibirás notificaciones por cualquier actividad"],"You will receive notifications only for comments in which you were @mentioned":["Recibirás notificaciones solo para los comentarios en los que se te mencionó"],"You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account":["No podrás actualizar o enviar código al proyecto a través de %{protocol} hasta que %{set_password_link} en tu cuenta"],"You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile":["No podrás actualizar o enviar código al proyecto a través de SSH hasta que %{add_ssh_key_link} en su perfil"],"Your name":["Tu nombre"],"day":["día","días"],"new merge request":["nueva solicitud de fusión"],"notification emails":["correos electrónicos de notificación"],"parent":["padre","padres"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/zh_CN/app.js b/app/assets/javascripts/locale/zh_CN/app.js index d1335cfbc0f..9c28e4e4627 100644 --- a/app/assets/javascripts/locale/zh_CN/app.js +++ b/app/assets/javascripts/locale/zh_CN/app.js @@ -1 +1 @@ -var locales = locales || {}; locales['zh_CN'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (China) (https://www.transifex.com/gitlab-zh/teams/75177/zh_CN/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_CN","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_CN","domain":"app","plural_forms":"nplurals=1; plural=0;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":["作者:"],"Cancel":[""],"Commit":["提交"],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["周期分析概述了项目从想法到产品实现的各阶段所需的时间。"],"CycleAnalyticsStage|Code":["编码"],"CycleAnalyticsStage|Issue":["议题"],"CycleAnalyticsStage|Plan":["计划"],"CycleAnalyticsStage|Production":["生产"],"CycleAnalyticsStage|Review":["评审"],"CycleAnalyticsStage|Staging":["预发布"],"CycleAnalyticsStage|Test":["测试"],"Delete":[""],"Deploy":["部署"],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["从创建议题到部署至生产环境"],"From merge request merge until deploy to production":["从合并请求被合并后到部署至生产环境"],"Interval Pattern":[""],"Introducing Cycle Analytics":["周期分析简介"],"Last %d day":["最后 %d 天"],"Last Pipeline":[""],"Limited to showing %d event at most":["最多显示 %d 个事件"],"Median":["中位数"],"New Issue":["新议题"],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":["数据不足"],"Not enough data":["数据不足"],"OpenedNDaysAgo|Opened":["开始于"],"Owner":[""],"Pipeline Health":["流水线健康指标"],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":["项目生命周期"],"Read more":["了解更多"],"Related Commits":["相关的提交"],"Related Deployed Jobs":["相关的部署作业"],"Related Issues":["相关的议题"],"Related Jobs":["相关的作业"],"Related Merge Requests":["相关的合并请求"],"Related Merged Requests":["相关已合并的合并请求"],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["显示 %d 个事件"],"Target Branch":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["编码阶段概述了从第一次提交到创建合并请求的时间。创建第一个合并请求后,数据将自动添加到此处。"],"The collection of events added to the data gathered for that stage.":["与该阶段相关的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["议题阶段概述了从创建议题到将议题设置里程碑或将议题添加到议题看板的时间。开始创建议题以查看此阶段的数据。"],"The phase of the development lifecycle.":["项目生命周期中的各个阶段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["计划阶段概述了从议题添加到日程后到推送首次提交的时间。当首次推送提交后,数据将自动添加到此处。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["生产阶段概述了从创建一个议题到将代码部署到生产环境的总时间。当完成想法到部署生产的循环,数据将自动添加到此处。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["评审阶段概述了从创建合并请求到被合并的时间。当创建第一个合并请求后,数据将自动添加到此处。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["预发布阶段概述了从合并请求被合并到部署至生产环境的总时间。首次部署到生产环境后,数据将自动添加到此处。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["测试阶段概述了GitLab CI为相关合并请求运行每个流水线所需的时间。当第一个流水线运行完成后,数据将自动添加到此处。"],"The time taken by each data entry gathered by that stage.":["该阶段每条数据所花的时间"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位数是一个数列中最中间的值。例如在 3、5、9 之间,中位数是 5。在 3、5、7、8 之间,中位数是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["议题被列入日程表的时间"],"Time before an issue starts implementation":["开始进行编码前的时间"],"Time between merge request creation and merge/close":["从创建合并请求到被合并或关闭的时间"],"Time until first merge request":["创建第一个合并请求之前的时间"],"Time|hr":["小时"],"Time|min":["分钟"],"Time|s":["秒"],"Total Time":["总时间"],"Total test time for all commits/merges":["所有提交和合并的总测试时间"],"Want to see the data? Please ask an administrator for access.":["权限不足。如需查看相关数据,请向管理员申请权限。"],"We don't have enough data to show this stage.":["该阶段的数据不足,无法显示。"],"You need permission.":["您需要相关的权限。"],"day":["天"]}}};
\ No newline at end of file +var locales = locales || {}; locales['zh_CN'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-06-15 21:59-0500","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","PO-Revision-Date":"2017-06-19 09:57-0400","Last-Translator":"Huang Tao <htve@outlook.com>","Language-Team":"Chinese (China) (https://translate.zanata.org/project/view/GitLab)","Language":"zh-CN","X-Generator":"Zanata 3.9.6","Plural-Forms":"nplurals=1; plural=0","lang":"zh_CN","domain":"app","plural_forms":"nplurals=1; plural=0"},"%{commit_author_link} committed %{commit_timeago}":["由 %{commit_author_link} 提交于 %{commit_timeago}"],"About auto deploy":["关于自动部署"],"Active":["启用"],"Activity":["活动"],"Add Changelog":["添加更新日志"],"Add Contribution guide":["添加贡献指南"],"Add License":["添加许可证"],"Add an SSH key to your profile to pull or push via SSH.":["新建一个用于推送或拉取的 SSH 秘钥到账号中。"],"Add new directory":["添加目录"],"Archived project! Repository is read-only":["项目已归档!存储库为只读状态"],"Are you sure you want to delete this pipeline schedule?":["确定要删除此流水线计划吗?"],"Attach a file by drag & drop or %{upload_link}":["拖放文件到此处或者 %{upload_link}"],"Branch":["分支"],"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}":["已创建分支 <strong>%{branch_name}</strong> 。如需设置自动部署, 请选择合适的 GitLab CI Yaml 模板并提交更改。%{link_to_autodeploy_doc}"],"Branches":["分支"],"Browse files":["浏览文件"],"ByAuthor|by":["作者:"],"CI configuration":["CI 配置"],"Cancel":["取消"],"ChangeTypeActionLabel|Pick into branch":["选择分支"],"ChangeTypeActionLabel|Revert in branch":["还原分支"],"ChangeTypeAction|Cherry-pick":["优选"],"ChangeTypeAction|Revert":["还原"],"Changelog":["更新日志"],"Charts":["统计图"],"Cherry-pick this commit":["优选此提交"],"Cherry-pick this merge request":["优选此合并请求"],"CiStatusLabel|canceled":["已取消"],"CiStatusLabel|created":["已创建"],"CiStatusLabel|failed":["已失败"],"CiStatusLabel|manual action":["手动操作"],"CiStatusLabel|passed":["已通过"],"CiStatusLabel|passed with warnings":["已通过但有警告"],"CiStatusLabel|pending":["等待中"],"CiStatusLabel|skipped":["已跳过"],"CiStatusLabel|waiting for manual action":["等待手动操作"],"CiStatusText|blocked":["已阻塞"],"CiStatusText|canceled":["已取消"],"CiStatusText|created":["已创建"],"CiStatusText|failed":["已失败"],"CiStatusText|manual":["手动操作"],"CiStatusText|passed":["已通过"],"CiStatusText|pending":["等待中"],"CiStatusText|skipped":["已跳过"],"CiStatus|running":["运行中"],"Commit":["提交"],"Commit message":["提交信息"],"CommitBoxTitle|Commit":["提交"],"CommitMessage|Add %{file_name}":["添加 %{file_name}"],"Commits":["提交"],"Commits|History":["历史"],"Committed by":["提交者:"],"Compare":["比较"],"Contribution guide":["贡献指南"],"Contributors":["贡献者"],"Copy URL to clipboard":["复制 URL 到剪贴板"],"Copy commit SHA to clipboard":["复制提交 SHA 的值到剪贴板"],"Create New Directory":["创建新目录"],"Create directory":["创建目录"],"Create empty bare repository":["创建空的存储库"],"Create merge request":["创建合并请求"],"Create new...":["创建..."],"CreateNewFork|Fork":["派生"],"CreateTag|Tag":["标签"],"Cron Timezone":["Cron 时区"],"Cron syntax":["Cron 语法"],"Custom notification events":["自定义通知事件"],"Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}.":["自定义通知级别继承自参与级别。使用自定义通知级别,您会收到参与级别及选定事件的通知。想了解更多信息,请查看 %{notification_link}."],"Cycle Analytics":["周期分析"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["周期分析概述了项目从想法到产品实现的各阶段所需的时间。"],"CycleAnalyticsStage|Code":["编码"],"CycleAnalyticsStage|Issue":["议题"],"CycleAnalyticsStage|Plan":["计划"],"CycleAnalyticsStage|Production":["生产"],"CycleAnalyticsStage|Review":["评审"],"CycleAnalyticsStage|Staging":["预发布"],"CycleAnalyticsStage|Test":["测试"],"Define a custom pattern with cron syntax":["使用 Cron 语法定义自定义模式"],"Delete":["删除"],"Deploy":["部署"],"Description":["描述"],"Directory name":["目录名称"],"Don't show again":["不再显示"],"Download":["下载"],"Download tar":["下载 tar"],"Download tar.bz2":["下载 tar.bz2"],"Download tar.gz":["下载 tar.gz"],"Download zip":["下载 zip"],"DownloadArtifacts|Download":["下载"],"DownloadCommit|Email Patches":["电子邮件补丁"],"DownloadCommit|Plain Diff":["差异文件"],"DownloadSource|Download":["下载"],"Edit":["编辑"],"Edit Pipeline Schedule %{id}":["编辑 %{id} 流水线计划"],"Every day (at 4:00am)":["每日执行(凌晨 4 点)"],"Every month (on the 1st at 4:00am)":["每月执行(每月 1 日凌晨 4 点)"],"Every week (Sundays at 4:00am)":["每周执行(周日凌晨 4 点)"],"Failed to change the owner":["无法变更所有者"],"Failed to remove the pipeline schedule":["无法删除流水线计划"],"Files":["文件"],"Find by path":["按路径查找"],"Find file":["查找文件"],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"Fork":["派生"],"ForkedFromProjectPath|Forked from":["派生自"],"From issue creation until deploy to production":["从创建议题到部署至生产环境"],"From merge request merge until deploy to production":["从合并请求被合并后到部署至生产环境"],"Go to your fork":["跳转到派生项目"],"GoToYourFork|Fork":["跳转到派生项目"],"Home":["首页"],"Housekeeping successfully started":["已开始维护"],"Import repository":["导入存储库"],"Interval Pattern":["循环周期"],"Introducing Cycle Analytics":["周期分析简介"],"LFSStatus|Disabled":["停用"],"LFSStatus|Enabled":["启用"],"Last %d day":["最近 %d 天"],"Last Pipeline":["最新流水线"],"Last Update":["最后更新"],"Last commit":["最后提交"],"Learn more in the":["了解更多"],"Learn more in the|pipeline schedules documentation":["流水线计划文档"],"Leave group":["退出群组"],"Leave project":["退出项目"],"Limited to showing %d event at most":["最多显示 %d 个事件"],"Median":["中位数"],"MissingSSHKeyWarningLink|add an SSH key":["新建 SSH 公钥"],"New Issue":["新建议题"],"New Pipeline Schedule":["创建流水线计划"],"New branch":["新建分支"],"New directory":["新建目录"],"New file":["新建文件"],"New issue":["新建议题"],"New merge request":["新建合并请求"],"New schedule":["新建计划"],"New snippet":["新建代码片段"],"New tag":["新建标签"],"No repository":["没有存储库"],"No schedules":["没有计划"],"Not available":["数据不足"],"Not enough data":["数据不足"],"Notification events":["通知事件"],"NotificationEvent|Close issue":["关闭议题"],"NotificationEvent|Close merge request":["关闭合并请求"],"NotificationEvent|Failed pipeline":["流水线失败"],"NotificationEvent|Merge merge request":["合并请求被合并"],"NotificationEvent|New issue":["新建议题"],"NotificationEvent|New merge request":["新建合并请求"],"NotificationEvent|New note":["新建评论"],"NotificationEvent|Reassign issue":["重新指派议题"],"NotificationEvent|Reassign merge request":["重新指派合并请求"],"NotificationEvent|Reopen issue":["重启议题"],"NotificationEvent|Successful pipeline":["流水线成功完成"],"NotificationLevel|Custom":["自定义"],"NotificationLevel|Disabled":["停用"],"NotificationLevel|Global":["全局"],"NotificationLevel|On mention":["提及"],"NotificationLevel|Participate":["参与"],"NotificationLevel|Watch":["关注"],"OfSearchInADropdown|Filter":["筛选"],"OpenedNDaysAgo|Opened":["开始于"],"Options":["操作"],"Owner":["所有者"],"Pipeline":["流水线"],"Pipeline Health":["流水线健康指标"],"Pipeline Schedule":["流水线计划"],"Pipeline Schedules":["流水线计划"],"PipelineSchedules|Activated":["是否启用"],"PipelineSchedules|Active":["已启用"],"PipelineSchedules|All":["所有"],"PipelineSchedules|Inactive":["未启用"],"PipelineSchedules|Next Run":["下次运行时间"],"PipelineSchedules|None":["无"],"PipelineSchedules|Provide a short description for this pipeline":["为此流水线提供简短描述"],"PipelineSchedules|Take ownership":["取得所有者"],"PipelineSchedules|Target":["目标"],"PipelineSheduleIntervalPattern|Custom":["自定义"],"Pipeline|with stage":["于阶段"],"Pipeline|with stages":["于阶段"],"Project '%{project_name}' queued for deletion.":["项目 '%{project_name}' 已进入删除队列。"],"Project '%{project_name}' was successfully created.":["项目 '%{project_name}' 已创建成功。"],"Project '%{project_name}' was successfully updated.":["项目 '%{project_name}' 已更新完成。"],"Project '%{project_name}' will be deleted.":["项目 '%{project_name}' 将被删除。"],"Project access must be granted explicitly to each user.":["项目访问权限必须明确授权给每个用户。"],"Project export could not be deleted.":["无法删除项目导出。"],"Project export has been deleted.":["项目导出已被删除。"],"Project export link has expired. Please generate a new export from your project settings.":["项目导出链接已过期。请从项目设置中重新生成项目导出。"],"Project export started. A download link will be sent by email.":["项目导出已开始。下载链接将通过电子邮件发送。"],"Project home":["项目首页"],"ProjectFeature|Disabled":["停用"],"ProjectFeature|Everyone with access":["任何对项目有访问权的人"],"ProjectFeature|Only team members":["只限团队成员"],"ProjectFileTree|Name":["名称"],"ProjectLastActivity|Never":["从未"],"ProjectLifecycle|Stage":["阶段"],"ProjectNetworkGraph|Graph":["分支图"],"Read more":["了解更多"],"Readme":["自述文件"],"RefSwitcher|Branches":["分支"],"RefSwitcher|Tags":["标签"],"Related Commits":["相关的提交"],"Related Deployed Jobs":["相关的部署作业"],"Related Issues":["相关的议题"],"Related Jobs":["相关的作业"],"Related Merge Requests":["相关的合并请求"],"Related Merged Requests":["相关已合并的合并请求"],"Remind later":["稍后提醒"],"Remove project":["删除项目"],"Request Access":["申请权限"],"Revert this commit":["还原此提交"],"Revert this merge request":["还原此合并请求"],"Save pipeline schedule":["保存流水线计划"],"Schedule a new pipeline":["新建流水线计划"],"Scheduling Pipelines":["流水线计划"],"Search branches and tags":["搜索分支和标签"],"Select Archive Format":["选择下载格式"],"Select a timezone":["选择时区"],"Select target branch":["选择目标分支"],"Set a password on your account to pull or push via %{protocol}":["为账号创建一个用于推送或拉取的 %{protocol} 密码。"],"Set up CI":["设置 CI"],"Set up Koding":["设置 Koding"],"Set up auto deploy":["设置自动部署"],"SetPasswordToCloneLink|set a password":["设置密码"],"Showing %d event":["显示 %d 个事件"],"Source code":["源代码"],"StarProject|Star":["星标"],"Start a %{new_merge_request} with these changes":["由此更改 %{new_merge_request}"],"Switch branch/tag":["切换分支/标签"],"Tag":["标签"],"Tags":["标签"],"Target Branch":["目标分支"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["编码阶段概述了从第一次提交到创建合并请求的时间。创建第一个合并请求后,数据将自动添加到此处。"],"The collection of events added to the data gathered for that stage.":["与该阶段相关的事件集合。"],"The fork relationship has been removed.":["派生关系已被删除。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["议题阶段概述了从创建议题到将议题添加到里程碑或议题看板所花费的时间。创建第一个议题后,数据将自动添加到此处.。"],"The phase of the development lifecycle.":["项目生命周期中的各个阶段。"],"The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.":["流水线计划会周期性重复运行指定分支或标签的流水线。这些流水线将根据其关联用户继承有限的项目访问权限。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["计划阶段概述了从议题添加到日程到推送首次提交的时间。当首次推送提交后,数据将自动添加到此处。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["生产阶段概述了从创建一个议题到将代码部署到生产环境的总时间。当完成想法到部署生产的循环,数据将自动添加到此处。"],"The project can be accessed by any logged in user.":["该项目允许已登录的用户访问。"],"The project can be accessed without any authentication.":["该项目允许任何人访问。"],"The repository for this project does not exist.":["此项目的存储库不存在。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["评审阶段概述了从创建合并请求到被合并的时间。当创建第一个合并请求后,数据将自动添加到此处。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["预发布阶段概述了从合并请求被合并到部署至生产环境的总时间。首次部署到生产环境后,数据将自动添加到此处。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["测试阶段概述了 GitLab CI 为相关合并请求运行每个流水线所需的时间。当第一个流水线运行完成后,数据将自动添加到此处。"],"The time taken by each data entry gathered by that stage.":["该阶段每条数据所花的时间"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位数是一个数列中最中间的值。例如在 3、5、9 之间,中位数是 5。在 3、5、7、8 之间,中位数是 (5 + 7)/ 2 = 6。"],"This means you can not push code until you create an empty repository or import existing one.":["在创建一个空的存储库或导入现有存储库之前,将无法推送代码。"],"Time before an issue gets scheduled":["议题被列入日程表的时间"],"Time before an issue starts implementation":["开始进行编码前的时间"],"Time between merge request creation and merge/close":["从创建合并请求到被合并或关闭的时间"],"Time until first merge request":["创建第一个合并请求之前的时间"],"Timeago|%s days ago":[" %s 天前"],"Timeago|%s days remaining":["剩余 %s 天"],"Timeago|%s hours remaining":["剩余 %s 小时"],"Timeago|%s minutes ago":[" %s 分钟前"],"Timeago|%s minutes remaining":["剩余 %s 分钟"],"Timeago|%s months ago":[" %s 个月前"],"Timeago|%s months remaining":["剩余 %s 月"],"Timeago|%s seconds remaining":["剩余 %s 秒"],"Timeago|%s weeks ago":[" %s 星期前"],"Timeago|%s weeks remaining":["剩余 %s 星期"],"Timeago|%s years ago":[" %s 年前"],"Timeago|%s years remaining":["剩余 %s 年"],"Timeago|1 day remaining":["剩余 1 天"],"Timeago|1 hour remaining":["剩余 1 小时"],"Timeago|1 minute remaining":["剩余 1 分钟"],"Timeago|1 month remaining":["剩余 1 个月"],"Timeago|1 week remaining":["剩余 1 星期"],"Timeago|1 year remaining":["剩余 1 年"],"Timeago|Past due":["逾期"],"Timeago|a day ago":[" 1 天前"],"Timeago|a month ago":[" 1 个月前"],"Timeago|a week ago":[" 1 星期前"],"Timeago|a while":["刚刚"],"Timeago|a year ago":[" 1 年前"],"Timeago|about %s hours ago":["约 %s 小时前"],"Timeago|about a minute ago":["约 1 分钟前"],"Timeago|about an hour ago":["约 1 小时前"],"Timeago|in %s days":[" %s 天后"],"Timeago|in %s hours":[" %s 小时后"],"Timeago|in %s minutes":[" %s 分钟后"],"Timeago|in %s months":[" %s 个月后"],"Timeago|in %s seconds":[" %s 秒后"],"Timeago|in %s weeks":[" %s 星期后"],"Timeago|in %s years":[" %s 年后"],"Timeago|in 1 day":[" 1 天后"],"Timeago|in 1 hour":[" 1 小时后"],"Timeago|in 1 minute":[" 1 分钟后"],"Timeago|in 1 month":[" 1 月后"],"Timeago|in 1 week":[" 1 星期后"],"Timeago|in 1 year":[" 1 年后"],"Timeago|less than a minute ago":["不到 1 分钟前"],"Time|hr":["小时"],"Time|min":["分钟"],"Time|s":["秒"],"Total Time":["总时间"],"Total test time for all commits/merges":["所有提交和合并的总测试时间"],"Unstar":["取消星标"],"Upload New File":["上传新文件"],"Upload file":["上传文件"],"Use your global notification setting":["使用全局通知设置"],"VisibilityLevel|Internal":["内部"],"VisibilityLevel|Private":["私有"],"VisibilityLevel|Public":["公开"],"Want to see the data? Please ask an administrator for access.":["权限不足。如需查看相关数据,请向管理员申请权限。"],"We don't have enough data to show this stage.":["该阶段的数据不足,无法显示。"],"Withdraw Access Request":["取消权限申请"],"You are going to remove %{project_name_with_namespace}.\\nRemoved project CANNOT be restored!\\nAre you ABSOLUTELY sure?":["即将要删除 %{project_name_with_namespace}。\\n已删除的项目无法恢复!\\n确定继续吗?"],"You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?":["即将删除与源项目 %{forked_from_project} 的派生关系。确定继续吗?"],"You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?":["即将 %{project_name_with_namespace} 转移给另一个所有者。确定继续吗?"],"You can only add files when you are on a branch":["只能在分支上添加文件"],"You have reached your project limit":["您已达到项目数量限制"],"You must sign in to star a project":["必须登录才能对项目加星标"],"You need permission.":["需要相关的权限。"],"You will not get any notifications via email":["不会收到任何通知邮件"],"You will only receive notifications for the events you choose":["只接收选择的事件通知"],"You will only receive notifications for threads you have participated in":["只接收参与的主题的通知"],"You will receive notifications for any activity":["接收所有活动的通知"],"You will receive notifications only for comments in which you were @mentioned":["只接收评论中提及(@)您的通知"],"You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account":["在账号中 %{set_password_link} 之前将无法通过 %{protocol} 拉取或推送代码。"],"You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile":["在账号中 %{add_ssh_key_link} 之前将无法通过 SSH 拉取或推送代码。"],"Your name":["您的名字"],"day":["天"],"new merge request":["新建合并请求"],"notification emails":["通知邮件"],"parent":["父级"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 894ed81b044..7bb2236017e 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -233,11 +233,18 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; } mountPipelinesView() { - this.commitPipelinesTable = new gl.CommitPipelinesTable().$mount(); + const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); + const CommitPipelinesTable = gl.CommitPipelinesTable; + this.commitPipelinesTable = new CommitPipelinesTable({ + propsData: { + endpoint: pipelineTableViewEl.dataset.endpoint, + helpPagePath: pipelineTableViewEl.dataset.helpPagePath, + }, + }).$mount(); + // $mount(el) replaces the el with the new rendered component. We need it in order to mount // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount - document.querySelector('#commit-pipeline-table-view') - .appendChild(this.commitPipelinesTable.$el); + pipelineTableViewEl.appendChild(this.commitPipelinesTable.$el); } loadDiff(source) { diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 07ede5ee913..3e07ec4d0aa 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -4,87 +4,7 @@ (function() { this.Milestone = (function() { - Milestone.updateIssue = function(li, issue_url, data) { - return $.ajax({ - type: "PUT", - url: issue_url, - data: data, - success: function(_data) { - return Milestone.successCallback(_data, li); - }, - error: function(data) { - return new Flash("Issue update failed", 'alert'); - }, - dataType: "json" - }); - }; - - Milestone.sortIssues = function(url, data) { - return $.ajax({ - type: "PUT", - url, - data: data, - success: function(_data) { - return Milestone.successCallback(_data); - }, - error: function() { - return new Flash("Issues update failed", 'alert'); - }, - dataType: "json" - }); - }; - - Milestone.sortMergeRequests = function(url, data) { - return $.ajax({ - type: "PUT", - url, - data: data, - success: function(_data) { - return Milestone.successCallback(_data); - }, - error: function(data) { - return new Flash("Issue update failed", 'alert'); - }, - dataType: "json" - }); - }; - - Milestone.updateMergeRequest = function(li, merge_request_url, data) { - return $.ajax({ - type: "PUT", - url: merge_request_url, - data: data, - success: function(_data) { - return Milestone.successCallback(_data, li); - }, - error: function(data) { - return new Flash("Issue update failed", 'alert'); - }, - dataType: "json" - }); - }; - - Milestone.successCallback = function(data, element) { - const $avatarContainer = $(element).find('.assignee-icon'); - $avatarContainer.empty(); - - if (data.assignees && data.assignees.length > 0) { - const $avatars = data.assignees.map((assignee) => { - const img_tag = $('<img/>'); - img_tag.attr('src', assignee.avatar_url); - img_tag.addClass('avatar s16'); - return img_tag; - }); - - $avatarContainer.append($avatars); - } - }; - function Milestone() { - this.issuesSortEndpoint = $('#tab-issues').data('sort-endpoint'); - this.mergeRequestsSortEndpoint = $('#tab-merge-requests').data('sort-endpoint'); - - this.bindIssuesSorting(); this.bindTabsSwitching(); // Load merge request tab if it is active @@ -94,22 +14,6 @@ this.loadInitialTab(); } - Milestone.prototype.bindIssuesSorting = function() { - if (!this.issuesSortEndpoint) return; - - $('#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed').each(function (i, el) { - this.createSortable(el, { - group: 'issue-list', - listEls: $('.issues-sortable-list'), - fieldName: 'issue', - sortCallback: (data) => { - Milestone.sortIssues(this.issuesSortEndpoint, data); - }, - updateCallback: Milestone.updateIssue, - }); - }.bind(this)); - }; - Milestone.prototype.bindTabsSwitching = function() { return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => { const $target = $(e.target); @@ -119,69 +23,6 @@ }); }; - Milestone.prototype.bindMergeRequestSorting = function() { - if (!this.mergeRequestsSortEndpoint) return; - - $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").each(function (i, el) { - this.createSortable(el, { - group: 'merge-request-list', - listEls: $(".merge_requests-sortable-list:not(#merge_requests-list-merged)"), - fieldName: 'merge_request', - sortCallback: (data) => { - Milestone.sortMergeRequests(this.mergeRequestsSortEndpoint, data); - }, - updateCallback: Milestone.updateMergeRequest, - }); - }.bind(this)); - }; - - Milestone.prototype.createSortable = function(el, opts) { - return Sortable.create(el, { - group: opts.group, - filter: '.is-disabled', - forceFallback: true, - onStart: function(e) { - opts.listEls.css('min-height', e.item.offsetHeight); - }, - onEnd: function () { - opts.listEls.css("min-height", "0px"); - }, - onUpdate: function(e) { - var ids = this.toArray(), - data; - - if (ids.length) { - data = ids.map(function(id) { - return 'sortable_' + opts.fieldName + '[]=' + id; - }).join('&'); - - opts.sortCallback(data); - } - }, - onAdd: function (e) { - var data, issuableId, issuableUrl, newState; - newState = e.to.dataset.state; - issuableUrl = e.item.dataset.url; - data = (function() { - switch (newState) { - case 'ongoing': - return `${opts.fieldName}[assignee_ids][]=${gon.current_user_id}`; - case 'unassigned': - return `${opts.fieldName}[assignee_ids][]=0`; - case 'closed': - return opts.fieldName + '[state_event]=close'; - } - })(); - if (e.from.dataset.state === 'closed') { - data += '&' + opts.fieldName + '[state_event]=reopen'; - } - - opts.updateCallback(e.item, issuableUrl, data); - this.options.onUpdate.call(this, e); - } - }); - }; - Milestone.prototype.loadInitialTab = function() { const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`); @@ -203,10 +44,6 @@ .done((data) => { $(tabElId).html(data.html); $target.addClass('is-loaded'); - - if (tabElId === '#tab-merge-requests') { - this.bindMergeRequestSorting(); - } }); } }; diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index d56cf959486..624dd336786 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -32,7 +32,7 @@ const normalizeNewlines = function(str) { (function() { this.Notes = (function() { const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; - const REGEX_SLASH_COMMANDS = /^\/\w+.*$/gm; + const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; Notes.interval = null; @@ -187,7 +187,7 @@ const normalizeNewlines = function(str) { if ($textarea.val() !== '') { return; } - myLastNote = $(`li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`, $textarea.closest('.note, #notes')); + myLastNote = $(`li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`, $textarea.closest('.note, .notes_holder, #notes')); if (myLastNote.length) { myLastNoteEditBtn = myLastNote.find('.js-note-edit'); return myLastNoteEditBtn.trigger('click', [true, myLastNote]); @@ -284,7 +284,7 @@ const normalizeNewlines = function(str) { return this.initRefresh(); }; - Notes.prototype.handleSlashCommands = function(noteEntity) { + Notes.prototype.handleQuickActions = function(noteEntity) { var votesBlock; if (noteEntity.commands_changes) { if ('merge' in noteEntity.commands_changes) { @@ -322,7 +322,9 @@ const normalizeNewlines = function(str) { Notes.updateNoteTargetSelector = function($note) { const hash = gl.utils.getLocationHash(); - $note.toggleClass('target', hash && $note.filter(`#${hash}`).length > 0); + // Needs to be an explicit true/false for the jQuery `toggleClass(force)` + const addTargetClass = Boolean(hash && $note.filter(`#${hash}`).length > 0); + $note.toggleClass('target', addTargetClass); }; /* @@ -1220,27 +1222,27 @@ const normalizeNewlines = function(str) { }; /** - * Identify if comment has any slash commands + * Identify if comment has any quick actions */ - Notes.prototype.hasSlashCommands = function(formContent) { - return REGEX_SLASH_COMMANDS.test(formContent); + Notes.prototype.hasQuickActions = function(formContent) { + return REGEX_QUICK_ACTIONS.test(formContent); }; /** - * Remove slash commands and leave comment with pure message + * Remove quick actions and leave comment with pure message */ - Notes.prototype.stripSlashCommands = function(formContent) { - return formContent.replace(REGEX_SLASH_COMMANDS, '').trim(); + Notes.prototype.stripQuickActions = function(formContent) { + return formContent.replace(REGEX_QUICK_ACTIONS, '').trim(); }; /** - * Gets appropriate description from slash commands found in provided `formContent` + * Gets appropriate description from quick actions found in provided `formContent` */ - Notes.prototype.getSlashCommandDescription = function (formContent, availableSlashCommands = []) { + Notes.prototype.getQuickActionDescription = function (formContent, availableQuickActions = []) { let tempFormContent; - // Identify executed slash commands from `formContent` - const executedCommands = availableSlashCommands.filter((command, index) => { + // Identify executed quick actions from `formContent` + const executedCommands = availableQuickActions.filter((command, index) => { const commandRegex = new RegExp(`/${command.name}`); return commandRegex.test(formContent); }); @@ -1298,7 +1300,7 @@ const normalizeNewlines = function(str) { }; /** - * Create Placeholder System Note DOM element populated with slash command description + * Create Placeholder System Note DOM element populated with quick action description */ Notes.prototype.createPlaceholderSystemNote = function ({ formContent, uniqueId }) { const $tempNote = $( @@ -1347,7 +1349,7 @@ const normalizeNewlines = function(str) { const { formData, formContent, formAction } = this.getFormData($form); let noteUniqueId; let systemNoteUniqueId; - let hasSlashCommands = false; + let hasQuickActions = false; let $notesContainer; let tempFormContent; @@ -1366,9 +1368,9 @@ const normalizeNewlines = function(str) { } tempFormContent = formContent; - if (this.hasSlashCommands(formContent)) { - tempFormContent = this.stripSlashCommands(formContent); - hasSlashCommands = true; + if (this.hasQuickActions(formContent)) { + tempFormContent = this.stripQuickActions(formContent); + hasQuickActions = true; } // Show placeholder note @@ -1385,10 +1387,10 @@ const normalizeNewlines = function(str) { } // Show placeholder system note - if (hasSlashCommands) { + if (hasQuickActions) { systemNoteUniqueId = _.uniqueId('tempSystemNote_'); $notesContainer.append(this.createPlaceholderSystemNote({ - formContent: this.getSlashCommandDescription(formContent, AjaxCache.get(gl.GfmAutoComplete.dataSources.commands)), + formContent: this.getQuickActionDescription(formContent, AjaxCache.get(gl.GfmAutoComplete.dataSources.commands)), uniqueId: systemNoteUniqueId, })); } @@ -1410,7 +1412,7 @@ const normalizeNewlines = function(str) { $notesContainer.find(`#${noteUniqueId}`).remove(); // Reset cached commands list when command is applied - if (hasSlashCommands) { + if (hasQuickActions) { $form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho'); } @@ -1444,7 +1446,7 @@ const normalizeNewlines = function(str) { } if (note.commands_changes) { - this.handleSlashCommands(note); + this.handleQuickActions(note); } $form.trigger('ajax:success', [note]); @@ -1452,7 +1454,7 @@ const normalizeNewlines = function(str) { // Submission failed, remove placeholder note and show Flash error message $notesContainer.find(`#${noteUniqueId}`).remove(); - if (hasSlashCommands) { + if (hasQuickActions) { $notesContainer.find(`#${systemNoteUniqueId}`).remove(); } diff --git a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js index 4d623763ca7..901adbe9fce 100644 --- a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js +++ b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js @@ -1,4 +1,7 @@ import Vue from 'vue'; +import Translate from '../../vue_shared/translate'; + +Vue.use(Translate); const inputNameAttribute = 'schedule[cron]'; @@ -72,11 +75,11 @@ export default { /> <label for="custom"> - Custom + {{ s__('PipelineSheduleIntervalPattern|Custom') }} </label> <span class="cron-syntax-link-wrap"> - (<a :href="cronSyntaxUrl" target="_blank">Cron syntax</a>) + (<a :href="cronSyntaxUrl" target="_blank">{{ __('Cron syntax') }}</a>) </span> </div> @@ -92,7 +95,7 @@ export default { /> <label class="label-light" for="every-day"> - Every day (at 4:00am) + {{ __('Every day (at 4:00am)') }} </label> </div> @@ -108,7 +111,7 @@ export default { /> <label class="label-light" for="every-week"> - Every week (Sundays at 4:00am) + {{ __('Every week (Sundays at 4:00am)') }} </label> </div> @@ -124,7 +127,7 @@ export default { /> <label class="label-light" for="every-month"> - Every month (on the 1st at 4:00am) + {{ __('Every month (on the 1st at 4:00am)') }} </label> </div> @@ -133,7 +136,7 @@ export default { id="schedule_cron" class="form-control inline cron-interval-input" type="text" - placeholder="Define a custom pattern with cron syntax" + :placeholder="__('Define a custom pattern with cron syntax')" required="true" v-model="cronInterval" :name="inputNameAttribute" diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js index 5109b110b31..c827b7402dc 100644 --- a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js +++ b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js @@ -1,6 +1,10 @@ +import Vue from 'vue'; import Cookies from 'js-cookie'; +import Translate from '../../vue_shared/translate'; import illustrationSvg from '../icons/intro_illustration.svg'; +Vue.use(Translate); + const cookieKey = 'pipeline_schedules_callout_dismissed'; export default { @@ -29,20 +33,18 @@ export default { </button> <div class="svg-container" v-html="illustrationSvg"></div> <div class="user-callout-copy"> - <h4>Scheduling Pipelines</h4> + <h4>{{ __('Scheduling Pipelines') }}</h4> <p> - The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. - Those scheduled pipelines will inherit limited project access based on their associated user. + {{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }} </p> - <p> Learn more in the + <p> {{ __('Learn more in the') }} <a :href="docsUrl" target="_blank" - rel="nofollow">pipeline schedules documentation</a>. <!-- oneline to prevent extra space before period --> + rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period --> </p> </div> </div> </div> `, }; - diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue index 37a6f02d8fd..abcd0c4ecea 100644 --- a/app/assets/javascripts/pipelines/components/async_button.vue +++ b/app/assets/javascripts/pipelines/components/async_button.vue @@ -1,9 +1,9 @@ <script> /* eslint-disable no-new, no-alert */ -/* global Flash */ -import '~/flash'; + import eventHub from '../event_hub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import tooltipMixin from '../../vue_shared/mixins/tooltip'; export default { props: { @@ -11,53 +11,42 @@ export default { type: String, required: true, }, - - service: { - type: Object, - required: true, - }, - title: { type: String, required: true, }, - icon: { type: String, required: true, }, - cssClass: { type: String, required: true, }, - confirmActionMessage: { type: String, required: false, }, }, - components: { loadingIcon, }, - + mixins: [ + tooltipMixin, + ], data() { return { isLoading: false, }; }, - computed: { iconClass() { return `fa fa-${this.icon}`; }, - buttonClass() { - return `btn has-tooltip ${this.cssClass}`; + return `btn ${this.cssClass}`; }, }, - methods: { onClick() { if (this.confirmActionMessage && confirm(this.confirmActionMessage)) { @@ -66,21 +55,11 @@ export default { this.makeRequest(); } }, - makeRequest() { this.isLoading = true; - $(this.$el).tooltip('destroy'); - - this.service.postAction(this.endpoint) - .then(() => { - this.isLoading = false; - eventHub.$emit('refreshPipelines'); - }) - .catch(() => { - this.isLoading = false; - new Flash('An error occured while making the request.'); - }); + $(this.$refs.tooltip).tooltip('destroy'); + eventHub.$emit('postAction', this.endpoint); }, }, }; @@ -95,10 +74,12 @@ export default { :aria-label="title" data-container="body" data-placement="top" + ref="tooltip" :disabled="isLoading"> <i :class="iconClass" - aria-hidden="true" /> + aria-hidden="true"> + </i> <loading-icon v-if="isLoading" /> </button> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index fed42d23112..01ae07aad65 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -1,15 +1,9 @@ <script> - import Visibility from 'visibilityjs'; import PipelinesService from '../services/pipelines_service'; - import eventHub from '../event_hub'; - import pipelinesTableComponent from '../../vue_shared/components/pipelines_table.vue'; + import pipelinesMixin from '../mixins/pipelines'; import tablePagination from '../../vue_shared/components/table_pagination.vue'; - import emptyState from './empty_state.vue'; - import errorState from './error_state.vue'; import navigationTabs from './navigation_tabs.vue'; import navigationControls from './nav_controls.vue'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import Poll from '../../lib/utils/poll'; export default { props: { @@ -20,13 +14,12 @@ }, components: { tablePagination, - pipelinesTableComponent, - emptyState, - errorState, navigationTabs, navigationControls, - loadingIcon, }, + mixins: [ + pipelinesMixin, + ], data() { const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; @@ -47,11 +40,6 @@ state: this.store.state, apiScope: 'all', pagenum: 1, - isLoading: false, - hasError: false, - isMakingRequest: false, - updateGraphDropdown: false, - hasMadeRequest: false, }; }, computed: { @@ -62,9 +50,6 @@ const scope = gl.utils.getParameterByName('scope'); return scope === null ? 'all' : scope; }, - shouldRenderErrorState() { - return this.hasError && !this.isLoading; - }, /** * The empty state should only be rendered when the request is made to fetch all pipelines @@ -106,7 +91,6 @@ this.state.pipelines.length && this.state.pageInfo.total > this.state.pageInfo.perPage; }, - hasCiEnabled() { return this.hasCi !== undefined; }, @@ -129,37 +113,7 @@ }, created() { this.service = new PipelinesService(this.endpoint); - - const poll = new Poll({ - resource: this.service, - method: 'getPipelines', - data: { page: this.pageParameter, scope: this.scopeParameter }, - successCallback: this.successCallback, - errorCallback: this.errorCallback, - notificationCallback: this.setIsMakingRequest, - }); - - if (!Visibility.hidden()) { - this.isLoading = true; - poll.makeRequest(); - } else { - // If tab is not visible we need to make the first request so we don't show the empty - // state without knowing if there are any pipelines - this.fetchPipelines(); - } - - Visibility.change(() => { - if (!Visibility.hidden()) { - poll.restart(); - } else { - poll.stop(); - } - }); - - eventHub.$on('refreshPipelines', this.fetchPipelines); - }, - beforeDestroy() { - eventHub.$off('refreshPipelines'); + this.requestData = { page: this.pageParameter, scope: this.scopeParameter }; }, methods: { /** @@ -174,15 +128,6 @@ return param; }, - fetchPipelines() { - if (!this.isMakingRequest) { - this.isLoading = true; - - this.service.getPipelines({ scope: this.scopeParameter, page: this.pageParameter }) - .then(response => this.successCallback(response)) - .catch(() => this.errorCallback()); - } - }, successCallback(resp) { const response = { headers: resp.headers, @@ -190,33 +135,14 @@ }; this.store.storeCount(response.body.count); - this.store.storePipelines(response.body.pipelines); this.store.storePagination(response.headers); - - this.isLoading = false; - this.updateGraphDropdown = true; - this.hasMadeRequest = true; - }, - - errorCallback() { - this.hasError = true; - this.isLoading = false; - this.updateGraphDropdown = false; - }, - - setIsMakingRequest(isMakingRequest) { - this.isMakingRequest = isMakingRequest; - - if (isMakingRequest) { - this.updateGraphDropdown = false; - } + this.setCommonData(response.body.pipelines); }, }, }; </script> <template> <div :class="cssClass"> - <div class="top-area scrolling-tabs-container inner-page-scroll-tabs" v-if="!isLoading && !shouldRenderEmptyState"> @@ -274,7 +200,6 @@ <pipelines-table-component :pipelines="state.pipelines" - :service="service" :update-graph-dropdown="updateGraphDropdown" /> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index 97b4de26214..a6fc4f04237 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -11,10 +11,6 @@ type: Array, required: true, }, - service: { - type: Object, - required: true, - }, }, components: { loadingIcon, @@ -31,17 +27,9 @@ $(this.$refs.tooltip).tooltip('destroy'); - this.service.postAction(endpoint) - .then(() => { - this.isLoading = false; - eventHub.$emit('refreshPipelines'); - }) - .catch(() => { - this.isLoading = false; - // eslint-disable-next-line no-new - new Flash('An error occured while making the request.'); - }); + eventHub.$emit('postAction', endpoint); }, + isActionDisabled(action) { if (action.playable === undefined) { return false; diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 884f1ce9689..5088d92209f 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -12,10 +12,6 @@ type: Array, required: true, }, - service: { - type: Object, - required: true, - }, updateGraphDropdown: { type: Boolean, required: false, @@ -57,7 +53,6 @@ v-for="model in pipelines" :key="model.id" :pipeline="model" - :service="service" :update-graph-dropdown="updateGraphDropdown" /> </div> diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 4d5ebe2e9ed..c3f1c426d8a 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -1,13 +1,13 @@ <script> /* eslint-disable no-param-reassign */ -import asyncButtonComponent from '../../pipelines/components/async_button.vue'; -import pipelinesActionsComponent from '../../pipelines/components/pipelines_actions.vue'; -import pipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts.vue'; -import ciBadge from './ci_badge_link.vue'; -import pipelineStage from '../../pipelines/components/stage.vue'; -import pipelineUrl from '../../pipelines/components/pipeline_url.vue'; -import pipelinesTimeago from '../../pipelines/components/time_ago.vue'; -import commitComponent from './commit.vue'; +import asyncButtonComponent from './async_button.vue'; +import pipelinesActionsComponent from './pipelines_actions.vue'; +import pipelinesArtifactsComponent from './pipelines_artifacts.vue'; +import ciBadge from '../../vue_shared/components/ci_badge_link.vue'; +import pipelineStage from './stage.vue'; +import pipelineUrl from './pipeline_url.vue'; +import pipelinesTimeago from './time_ago.vue'; +import commitComponent from '../../vue_shared/components/commit.vue'; /** * Pipeline table row. @@ -20,10 +20,6 @@ export default { type: Object, required: true, }, - service: { - type: Object, - required: true, - }, updateGraphDropdown: { type: Boolean, required: false, @@ -271,7 +267,6 @@ export default { <pipelines-actions-component v-if="pipeline.details.manual_actions.length" :actions="pipeline.details.manual_actions" - :service="service" /> <pipelines-artifacts-component @@ -282,7 +277,6 @@ export default { <async-button-component v-if="pipeline.flags.retryable" - :service="service" :endpoint="pipeline.retry_path" css-class="js-pipelines-retry-button btn-default btn-retry" title="Retry" @@ -291,7 +285,6 @@ export default { <async-button-component v-if="pipeline.flags.cancelable" - :service="service" :endpoint="pipeline.cancel_path" css-class="js-pipelines-cancel-button btn-remove" title="Cancel" diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js new file mode 100644 index 00000000000..9adc15e6266 --- /dev/null +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -0,0 +1,103 @@ +/* global Flash */ +import '~/flash'; +import Visibility from 'visibilityjs'; +import Poll from '../../lib/utils/poll'; +import emptyState from '../components/empty_state.vue'; +import errorState from '../components/error_state.vue'; +import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import pipelinesTableComponent from '../components/pipelines_table.vue'; +import eventHub from '../event_hub'; + +export default { + components: { + pipelinesTableComponent, + errorState, + emptyState, + loadingIcon, + }, + computed: { + shouldRenderErrorState() { + return this.hasError && !this.isLoading; + }, + }, + data() { + return { + isLoading: false, + hasError: false, + isMakingRequest: false, + updateGraphDropdown: false, + hasMadeRequest: false, + }; + }, + beforeMount() { + this.poll = new Poll({ + resource: this.service, + method: 'getPipelines', + data: this.requestData ? this.requestData : undefined, + successCallback: this.successCallback, + errorCallback: this.errorCallback, + notificationCallback: this.setIsMakingRequest, + }); + + if (!Visibility.hidden()) { + this.isLoading = true; + this.poll.makeRequest(); + } else { + // If tab is not visible we need to make the first request so we don't show the empty + // state without knowing if there are any pipelines + this.fetchPipelines(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + this.poll.restart(); + } else { + this.poll.stop(); + } + }); + + eventHub.$on('refreshPipelines', this.fetchPipelines); + eventHub.$on('postAction', this.postAction); + }, + beforeDestroy() { + eventHub.$off('refreshPipelines'); + eventHub.$on('postAction', this.postAction); + }, + destroyed() { + this.poll.stop(); + }, + methods: { + fetchPipelines() { + if (!this.isMakingRequest) { + this.isLoading = true; + + this.service.getPipelines(this.requestData) + .then(response => this.successCallback(response)) + .catch(() => this.errorCallback()); + } + }, + setCommonData(pipelines) { + this.store.storePipelines(pipelines); + this.isLoading = false; + this.updateGraphDropdown = true; + this.hasMadeRequest = true; + }, + errorCallback() { + this.hasError = true; + this.isLoading = false; + this.updateGraphDropdown = false; + }, + setIsMakingRequest(isMakingRequest) { + this.isMakingRequest = isMakingRequest; + + if (isMakingRequest) { + this.updateGraphDropdown = false; + } + }, + postAction(endpoint) { + this.service.postAction(endpoint) + .then(() => eventHub.$emit('refreshPipelines')) + .catch(() => new Flash('An error occured while making the request.')); + }, + }, +}; diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index 4a3df2fd465..141333b2b4d 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -3,7 +3,7 @@ // MarkdownPreview // // Handles toggling the "Write" and "Preview" tab clicks, rendering the preview -// (including the explanation of slash commands), and showing a warning when +// (including the explanation of quick actions), and showing a warning when // more than `x` users are referenced. // (function () { diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index b71c3097706..da7c0c5a36c 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -7,6 +7,13 @@ import Cookies from 'js-cookie'; function Sidebar(currentUser) { this.toggleTodo = this.toggleTodo.bind(this); this.sidebar = $('aside'); + + this.$sidebarInner = this.sidebar.find('.issuable-sidebar'); + this.$navGitlab = $('.navbar-gitlab'); + this.$layoutNav = $('.layout-nav'); + this.$subScroll = $('.sub-nav-scroll'); + this.$rightSidebar = $('.js-right-sidebar'); + this.removeListeners(); this.addEventListeners(); } @@ -21,14 +28,15 @@ import Cookies from 'js-cookie'; Sidebar.prototype.addEventListeners = function() { const $document = $(document); - const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight, 10); + const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 20); + const debouncedSetSidebarHeight = _.debounce(this.setSidebarHeight.bind(this), 200); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); $(window).on('resize', () => throttledSetSidebarHeight()); - $document.on('scroll', () => throttledSetSidebarHeight()); + $document.on('scroll', () => debouncedSetSidebarHeight()); $document.on('click', '.js-sidebar-toggle', function(e, triggered) { var $allGutterToggleIcons, $this, $thisIcon; e.preventDefault(); @@ -207,13 +215,14 @@ import Cookies from 'js-cookie'; }; Sidebar.prototype.setSidebarHeight = function() { - const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + $('.sub-nav-scroll').outerHeight(); - const $rightSidebar = $('.js-right-sidebar'); + const $navHeight = this.$navGitlab.outerHeight() + this.$layoutNav.outerHeight() + (this.$subScroll ? this.$subScroll.outerHeight() : 0); const diff = $navHeight - $(window).scrollTop(); if (diff > 0) { - $rightSidebar.outerHeight($(window).height() - diff); + this.$rightSidebar.outerHeight($(window).height() - diff); + this.$sidebarInner.height('100%'); } else { - $rightSidebar.outerHeight('100%'); + this.$rightSidebar.outerHeight('100%'); + this.$sidebarInner.height(''); } }; diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js index e67f449e1a2..59ff2a86293 100644 --- a/app/assets/javascripts/settings_panels.js +++ b/app/assets/javascripts/settings_panels.js @@ -1,11 +1,28 @@ +function expandSectionParent($section, $content) { + $section.addClass('expanded'); + $content.off('animationend.expandSectionParent'); +} + function expandSection($section) { $section.find('.js-settings-toggle').text('Close'); - $section.find('.settings-content').addClass('expanded').off('scroll').scrollTop(0); + + const $content = $section.find('.settings-content'); + $content.addClass('expanded').off('scroll.expandSection').scrollTop(0); + + if ($content.hasClass('no-animate')) { + expandSectionParent($section, $content); + } else { + $content.on('animationend.expandSectionParent', () => expandSectionParent($section, $content)); + } } function closeSection($section) { $section.find('.js-settings-toggle').text('Expand'); - $section.find('.settings-content').removeClass('expanded').on('scroll', () => expandSection($section)); + + const $content = $section.find('.settings-content'); + $content.removeClass('expanded').on('scroll.expandSection', () => expandSection($section)); + + $section.removeClass('expanded'); } function toggleSection($section) { @@ -21,7 +38,7 @@ function toggleSection($section) { export default function initSettingsPanels() { $('.settings').each((i, elm) => { const $section = $(elm); - $section.on('click', '.js-settings-toggle', () => toggleSection($section)); - $section.find('.settings-content:not(.expanded)').on('scroll', () => expandSection($section)); + $section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section)); + $section.find('.settings-content:not(.expanded)').on('scroll.expandSection', () => expandSection($section)); }); } diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.js b/app/assets/javascripts/sidebar/components/time_tracking/help_state.js index b2a77462fe0..142ad437509 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.js @@ -15,10 +15,10 @@ export default { <div class="time-tracking-help-state"> <div class="time-tracking-info"> <h4> - Track time with slash commands + Track time with quick actions </h4> <p> - Slash commands can be used in the issues description and comment boxes. + Quick actions can be used in the issues description and comment boxes. </p> <p> <code> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js index 244b67b3ad9..650e935b116 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js @@ -16,10 +16,10 @@ export default { 'issuable-time-tracker': timeTracker, }, methods: { - listenForSlashCommands() { - $(document).on('ajax:success', '.gfm-form', this.slashCommandListened); + listenForQuickActions() { + $(document).on('ajax:success', '.gfm-form', this.quickActionListened); }, - slashCommandListened(e, data) { + quickActionListened(e, data) { const subscribedCommands = ['spend_time', 'time_estimate']; let changedCommands; if (data !== undefined) { @@ -35,7 +35,7 @@ export default { }, }, mounted() { - this.listenForSlashCommands(); + this.listenForQuickActions(); }, template: ` <div class="block"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js index 8155218681c..76cb71b6c12 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js @@ -1,5 +1,5 @@ -import statusCodes from '~/lib/utils/http_status'; -import { bytesToMiB } from '~/lib/utils/number_utils'; +import statusCodes from '../../lib/utils/http_status'; +import { bytesToMiB } from '../../lib/utils/number_utils'; import MemoryGraph from '../../vue_shared/components/memory_graph'; import MRWidgetService from '../services/mr_widget_service'; diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index fefe5575d9b..95a08c960ea 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -254,7 +254,7 @@ } .landing { - margin-bottom: $gl-padding; + margin: $gl-padding auto; overflow: hidden; display: flex; position: relative; diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index b26d8fbd5fe..da03e4f5b5e 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -63,6 +63,7 @@ background-color: $gray-light; text-align: right; padding: 8px $gl-padding; + border-bottom: 1px solid $border-color; @media (max-width: $screen-xs-max) { text-align: left; diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index cfbaaaa04c7..767cf5ffea5 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -152,7 +152,7 @@ } .value-container { - background-color: $filter-value-selected-color; + box-shadow: inset 0 0 0 100px $filtered-search-term-shadow-color; } } @@ -236,9 +236,6 @@ width: 35px; background-color: $white-light; border: none; - position: static; - right: 0; - height: 100%; outline: none; z-index: 1; diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index a78179e727f..61e3897f369 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -125,10 +125,11 @@ label { .select-wrapper { position: relative; - .fa-caret-down { + .fa-chevron-down { position: absolute; + font-size: 10px; right: 10px; - top: 10px; + top: 12px; color: $gray-darkest; pointer-events: none; } @@ -138,6 +139,12 @@ label { padding-left: 10px; padding-right: 10px; -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + + &::-ms-expand { + display: none; + } } .form-control-inline { diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 80691a234f8..b21bcc22a87 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -174,3 +174,14 @@ white-space: nowrap; } } + +@media(max-width: $screen-xs-max) { + .atwho-view-ul { + width: 350px; + } + + .atwho-view ul li { + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 3787ef370b2..28b2a7cfacd 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -45,8 +45,7 @@ li { display: flex; - a, - .btn-link { + a { padding: $gl-btn-padding; padding-bottom: 11px; font-size: 14px; @@ -68,29 +67,7 @@ } } - .btn-link { - padding-top: 16px; - padding-left: 15px; - padding-right: 15px; - border-left: none; - border-right: none; - border-top: none; - border-radius: 0; - - &:hover, - &:active, - &:focus { - background-color: transparent; - } - - &:active { - outline: 0; - box-shadow: none; - } - } - - &.active a, - &.active .btn-link { + &.active a { border-bottom: 2px solid $link-underline-blue; color: $black; font-weight: 600; diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index fa364e68d22..e8d69e62194 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -1,54 +1,60 @@ .panel { margin-bottom: $gl-padding; +} + +.panel-slim { + @extend .panel; + margin-bottom: $gl-vert-padding; +} + + +.panel-heading { + padding: $gl-vert-padding $gl-padding; + line-height: 36px; + + .controls { + margin-top: -2px; + float: right; + } + + .dropdown-menu-toggle { + line-height: 20px; + } - .panel-heading { - padding: $gl-vert-padding $gl-padding; - line-height: 36px; - - .controls { - margin-top: -2px; - float: right; - } - - .dropdown-menu-toggle { - line-height: 20px; - } - - .badge { - margin-top: -2px; - margin-left: 5px; - } - - &.split { - display: flex; - align-items: center; - } - - .left { - flex: 1 1 auto; - } - - .right { - flex: 0 0 auto; - text-align: right; - } + .badge { + margin-top: -2px; + margin-left: 5px; } - .panel-empty-heading { - border-bottom: 0; + &.split { + display: flex; + align-items: center; } - .panel-body { - padding: $gl-padding; + .left { + flex: 1 1 auto; + } - .form-actions { - margin: -$gl-padding; - margin-top: $gl-padding; - } + .right { + flex: 0 0 auto; + text-align: right; } +} + +.panel-empty-heading { + border-bottom: 0; +} + +.panel-body { + padding: $gl-padding; - .panel-title { - font-size: inherit; - line-height: inherit; + .form-actions { + margin: -$gl-padding; + margin-top: $gl-padding; } } + +.panel-title { + font-size: inherit; + line-height: inherit; +} diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 1b20c35ad98..40e654f4838 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -18,19 +18,28 @@ background-image: none; background-color: transparent; border: none; - padding-top: 6px; - padding-right: 10px; + padding-top: 12px; + padding-right: 20px; + font-size: 10px; b { - display: inline-block; - width: 0; - height: 0; - margin-left: 2px; - vertical-align: middle; - border-top: 5px dashed; - border-right: 5px solid transparent; - border-left: 5px solid transparent; + display: none; + } + + &::after { + content: "\f078"; + position: absolute; + z-index: 1; + text-align: center; + pointer-events: none; + box-sizing: border-box; color: $gray-darkest; + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index d4421e3af74..5cf9330b8f8 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -97,17 +97,19 @@ .issues-bulk-update.right-sidebar { @include maintain-sidebar-dimensions; - transition: right $sidebar-transition-duration; - right: -$gutter-width; + width: 0; + padding: 0; + transition: width $sidebar-transition-duration; &.right-sidebar-expanded { @include maintain-sidebar-dimensions; - right: 0; + width: $gutter-width; } &.right-sidebar-collapsed { @include maintain-sidebar-dimensions; - right: -$gutter-width; + width: 0; + padding: 0; .block { padding: 16px 0; @@ -118,5 +120,6 @@ .issuable-sidebar { padding: 0 3px; + width: calc(100% + 35px); } } diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 10881987038..3d68a50f91f 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -44,6 +44,10 @@ &:target, &.target { background: $line-target-blue; + + &.system-note .note-body .note-text.system-note-commit-list::after { + background: linear-gradient(rgba($line-target-blue, 0.1) -100px, $line-target-blue 100%); + } } .avatar { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 49ba0108228..13c9c6c9fb3 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -282,6 +282,7 @@ $dropdown-toggle-active-border-color: darken($border-color, 14%); /* * Filtered Search */ +$filtered-search-term-shadow-color: rgba(0, 0, 0, 0.09); $dropdown-hover-color: $blue-400; /* @@ -322,6 +323,7 @@ $note-disabled-comment-color: #b2b2b2; $note-targe3-outside: #fffff0; $note-targe3-inside: #ffffd3; $note-line2-border: #ddd; +$note-icon-gutter-width: 55px; /* diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 7bec4bd5f56..3039732ca5b 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -4,7 +4,7 @@ position: relative; .landing { - margin-top: 10px; + margin-top: 0; .inner-content { white-space: normal; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 72d73b89a2a..6f6c6839975 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -90,8 +90,6 @@ } .explore-groups.landing { - margin-top: 10px; - .inner-content { padding: 0; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index b3f310ff67d..20f2eec9af5 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -204,7 +204,7 @@ .issuable-sidebar { width: calc(100% + 100px); - height: 100%; + height: calc(100% - #{$header-height}); overflow-y: scroll; overflow-x: hidden; -webkit-overflow-scrolling: touch; @@ -729,33 +729,3 @@ } } } - -.confidential-issue-warning { - background-color: $gl-gray; - border-radius: 3px; - padding: $gl-btn-padding $gl-padding; - margin-top: $gl-padding-top; - font-size: 14px; - color: $white-light; - - .fa { - margin-right: 8px; - } - - a { - color: $white-light; - text-decoration: underline; - } - - &.affix { - position: static; - width: initial; - - @media (min-width: $screen-sm-min) { - position: sticky; - position: -webkit-sticky; - top: 60px; - z-index: 200; - } - } -} diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 335e587b8f4..55e0ee1936e 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -111,8 +111,8 @@ } } -.issues-sortable-list, -.merge_requests-sortable-list { +.milestone-issues-list, +.milestone-merge_requests-list { .issuable-detail { display: block; margin-top: 7px; @@ -197,6 +197,4 @@ .issuable-row { background-color: $white-light; - cursor: -webkit-grab; - cursor: grab; } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index aa307414737..9877ed2cfd6 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -103,6 +103,42 @@ } } +.confidential-issue-warning { + background-color: $gray-normal; + border-radius: 3px; + padding: 3px 12px; + margin: auto; + margin-top: 0; + text-align: center; + font-size: 12px; + align-items: center; + + @media (max-width: $screen-md-max) { + // On smaller devices the warning becomes the fourth item in the list, + // rather than centering, and grows to span the full width of the + // comment area. + order: 4; + margin: 6px auto; + width: 100%; + } + + .fa { + margin-right: 8px; + } +} + +.right-sidebar-expanded { + .confidential-issue-warning { + // When the sidebar is open the warning becomes the fourth item in the list, + // rather than centering, and grows to span the full width of the + // comment area. + order: 4; + margin: 6px auto; + width: 100%; + } +} + + .discussion-form { padding: $gl-padding-top $gl-padding $gl-padding; background-color: $white-light; @@ -112,8 +148,20 @@ padding: 6px 0; } -.notes-form > li { - border: 0; +.notes.notes-form > li.timeline-entry { + @include notes-media('max', $screen-sm-max) { + padding: 0; + } + + .timeline-content { + @include notes-media('max', $screen-sm-max) { + margin: 0; + } + } + + .timeline-entry-inner { + border: 0; + } } .note-edit-form { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index a0442463390..f0dbe4249c5 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -14,16 +14,6 @@ ul.notes { margin: 0; padding: 0; - .timeline-content { - margin-left: 55px; - - &.timeline-content-form { - @include notes-media('max', $screen-sm-max) { - margin-left: 0; - } - } - } - .note-created-ago, .note-updated-at { white-space: nowrap; @@ -46,15 +36,47 @@ ul.notes { } } - > li { - padding: $gl-padding $gl-btn-padding; + > li { // .timeline-entry + padding: 0; display: block; position: relative; - border-bottom: 1px solid $white-normal; + border-bottom: 0; + + @include notes-media('min', $screen-sm-min) { + padding-left: $note-icon-gutter-width; + } + + .timeline-entry-inner { + padding: $gl-padding $gl-btn-padding; + border-bottom: 1px solid $white-normal; + } - &:last-child { - // Override `.timeline > li:last-child { border-bottom: none; }` + &:target, + &.target { border-bottom: 1px solid $white-normal; + + &:not(:first-child) { + border-top: 1px solid $white-normal; + margin-top: -1px; + } + + .timeline-entry-inner { + border-bottom: 0; + } + } + + .timeline-icon { + @include notes-media('min', $screen-sm-min) { + margin-left: -$note-icon-gutter-width; + } + } + + .timeline-content { + margin-left: $note-icon-gutter-width; + + @include notes-media('min', $screen-sm-min) { + margin-left: 0; + } } &.being-posted { @@ -73,7 +95,7 @@ ul.notes { } &.note-discussion { - &.timeline-entry { + .timeline-entry-inner { padding: $gl-padding 10px; } } @@ -152,13 +174,8 @@ ul.notes { .system-note { font-size: 14px; - padding-left: 0; clear: both; - @include notes-media('min', $screen-sm-min) { - margin-left: 65px; - } - .note-header-info { padding-bottom: 0; } @@ -192,13 +209,16 @@ ul.notes { .timeline-icon { float: left; + @include notes-media('min', $screen-sm-min) { + margin-left: 0; + width: auto; + } + svg { width: 16px; height: 16px; fill: $gray-darkest; - position: absolute; - left: 0; - top: 2px; + margin-top: 2px; } } @@ -250,7 +270,7 @@ ul.notes { &::after { content: ''; width: 100%; - height: 67px; + height: 70px; position: absolute; left: 0; bottom: 0; @@ -509,11 +529,6 @@ ul.notes { display: inline; line-height: 20px; - @include notes-media('min', $screen-sm-min) { - margin-left: 10px; - line-height: 24px; - } - .fa { color: $gray-darkest; position: relative; @@ -644,15 +659,12 @@ ul.notes { .discussion-body, .diff-file { .notes .note { - padding-left: $gl-padding; - padding-right: $gl-padding; - - &.system-note { - padding-left: 0; + border-bottom: 1px solid $white-normal; - @media (min-width: $screen-sm-min) { - margin-left: 70px; - } + .timeline-entry-inner { + padding-left: $gl-padding; + padding-right: $gl-padding; + border-bottom: none; } } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 062665bc634..562ecbc6986 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -380,7 +380,7 @@ a.deploy-project-label { padding: 0; background: transparent; border: none; - line-height: 36px; + line-height: 34px; margin: 0; > li + li::before { diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 085706a5001..d69a8e0995c 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -29,6 +29,10 @@ &:first-of-type { margin-top: 10px; } + + &.expanded { + overflow: visible; + } } .settings-header { diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index ab63225147f..ce1a13c6afa 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,6 +1,72 @@ .tree-holder { - > .nav-block { - margin: 11px 0; + .nav-block { + margin: 10px 0; + + @media (min-width: $screen-sm-min) { + display: flex; + + .tree-ref-container { + flex: 1; + } + + .tree-controls { + text-align: right; + + .btn-group { + margin-left: 10px; + } + } + + .tree-ref-holder { + float: left; + margin-right: 15px; + } + + .repo-breadcrumb { + li:last-of-type { + position: relative; + } + } + + .add-to-tree-dropdown { + position: absolute; + left: 18px; + } + } + } + + @media (max-width: $screen-xs-max) { + .repo-breadcrumb { + margin-top: 10px; + position: relative; + + .dropdown-menu { + min-width: 100%; + width: 100%; + left: inherit; + right: 0; + } + } + + .add-to-tree-dropdown { + position: absolute; + left: 0; + right: 0; + } + + .tree-controls { + margin-bottom: 10px; + + .btn, + .dropdown, + .btn-group { + width: 100%; + } + + .btn { + margin: 10px 0 0; + } + } } .file-finder { @@ -131,11 +197,6 @@ } } -.tree-ref-holder { - float: left; - margin-right: 15px; -} - .blob-commit-info { list-style: none; margin: 0; @@ -159,16 +220,6 @@ color: $md-link-color; } -.tree-controls { - float: right; - position: relative; - z-index: 2; - - .project-action-button { - margin-left: $btn-side-margin; - } -} - .repo-charts { .sub-header { margin: 20px 0; diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 36ad307a93b..1a9904bbe57 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -97,8 +97,8 @@ module CreatesCommit def merge_request_exists? return @merge_request if defined?(@merge_request) - @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened. - find_by(source_project_id: @project_to_commit_into, source_branch: @branch_name, target_branch: @start_branch) + @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened + .find_by(source_project_id: @project_to_commit_into, source_branch: @branch_name, target_branch: @start_branch) end def different_project? diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb index 8d07780f6c2..47d9ae350ae 100644 --- a/app/controllers/concerns/membership_actions.rb +++ b/app/controllers/concerns/membership_actions.rb @@ -15,8 +15,8 @@ module MembershipActions end def destroy - Members::DestroyService.new(membershipable, current_user, params). - execute(:all) + Members::DestroyService.new(membershipable, current_user, params) + .execute(:all) respond_to do |format| format.html do @@ -42,8 +42,8 @@ module MembershipActions end def leave - member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id). - execute(:all) + member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id) + .execute(:all) notice = if member.request? diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb index b2536a1c949..1ff785ac2ca 100644 --- a/app/controllers/concerns/milestone_actions.rb +++ b/app/controllers/concerns/milestone_actions.rb @@ -6,7 +6,7 @@ module MilestoneActions format.html { redirect_to milestone_redirect_path } format.json do render json: tabs_json("shared/milestones/_merge_requests_tab", { - merge_requests: @milestone.merge_requests, + merge_requests: @milestone.sorted_merge_requests, show_project_name: true }) end diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index 641c502dbe4..91c1e4dff79 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -22,8 +22,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController end def starred - @projects = load_projects(params.merge(starred: true)). - includes(:forked_from_project, :tags).page(params[:page]) + @projects = load_projects(params.merge(starred: true)) + .includes(:forked_from_project, :tags).page(params[:page]) @groups = [] @@ -45,8 +45,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController end def load_projects(finder_params) - ProjectsFinder.new(params: finder_params, current_user: current_user). - execute.includes(:route, namespace: :route) + ProjectsFinder.new(params: finder_params, current_user: current_user) + .execute.includes(:route, namespace: :route) end def load_events diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 8f1870759e4..741879dee35 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -49,7 +49,7 @@ class Explore::ProjectsController < Explore::ApplicationController private def load_projects - ProjectsFinder.new(current_user: current_user, params: params). - execute.includes(:route, namespace: :route) + ProjectsFinder.new(current_user: current_user, params: params) + .execute.includes(:route, namespace: :route) end end diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 11db164b3fa..4bceb1d67a3 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -11,8 +11,8 @@ class JwtController < ApplicationController service = SERVICES[params[:service]] return head :not_found unless service - result = service.new(@authentication_result.project, @authentication_result.actor, auth_params). - execute(authentication_abilities: @authentication_result.authentication_abilities) + result = service.new(@authentication_result.project, @authentication_result.actor, auth_params) + .execute(authentication_abilities: @authentication_result.authentication_abilities) render json: result, status: result[:http_status] end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 2a8c8ca4bad..b82681b197e 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -144,7 +144,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController end def log_audit_event(user, options = {}) - AuditEventService.new(user, user, options). - for_authentication.security_event + AuditEventService.new(user, user, options) + .for_authentication.security_event end end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 72f34930ca8..f98a9e24de1 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -49,9 +49,9 @@ class ProfilesController < Profiles::ApplicationController end def audit_log - @events = AuditEvent.where(entity_type: "User", entity_id: current_user.id). - order("created_at DESC"). - page(params[:page]) + @events = AuditEvent.where(entity_type: "User", entity_id: current_user.id) + .order("created_at DESC") + .page(params[:page]) end def update_username diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 603a51266da..3d7ce4f0222 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -53,9 +53,21 @@ class Projects::ApplicationController < ApplicationController end end + def check_project_feature_available!(feature) + render_404 unless project.feature_available?(feature, current_user) + end + + def check_issuables_available! + render_404 unless project.feature_available?(:issues, current_user) || + project.feature_available?(:merge_requests, current_user) + end + def method_missing(method_sym, *arguments, &block) - if method_sym.to_s =~ /\Aauthorize_(.*)!\z/ + case method_sym.to_s + when /\Aauthorize_(.*)!\z/ authorize_action!($1.to_sym) + when /\Acheck_(.*)_available!\z/ + check_project_feature_available!($1.to_sym) else super end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 66e6a9a451c..a82d6fd5a4a 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -187,7 +187,7 @@ class Projects::BlobController < Projects::ApplicationController end def set_last_commit_sha - @last_commit_sha = Gitlab::Git::Commit. - last_for_path(@repository, @ref, @path).sha + @last_commit_sha = Gitlab::Git::Commit + .last_for_path(@repository, @ref, @path).sha end end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 70b06cfd9b4..94a752c21eb 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -37,8 +37,8 @@ class Projects::BranchesController < Projects::ApplicationController redirect_to_autodeploy = project.empty_repo? && project.deployment_services.present? - result = CreateBranchService.new(project, current_user). - execute(branch_name, ref) + result = CreateBranchService.new(project, current_user) + .execute(branch_name, ref) if params[:issue_iid] issue = IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:issue_iid]) diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index f33797ca310..37b5a6e9d48 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -18,11 +18,11 @@ class Projects::CommitsController < Projects::ApplicationController @repository.commits(@ref, path: @path, limit: @limit, offset: @offset) end - @note_counts = project.notes.where(commit_id: @commits.map(&:id)). - group(:commit_id).count + @note_counts = project.notes.where(commit_id: @commits.map(&:id)) + .group(:commit_id).count - @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened. - find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref) + @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened + .find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref) respond_to do |format| format.html diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 88dd600e5fe..ef400c4d745 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -61,7 +61,7 @@ class Projects::CompareController < Projects::ApplicationController end def merge_request - @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened. - find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref) + @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened + .find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref) end end diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 7f1469e107d..c2e621fa190 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -6,7 +6,7 @@ class Projects::DeployKeysController < Projects::ApplicationController before_action :authorize_admin_project! before_action :authorize_update_deploy_key!, only: [:edit, :update] - layout "project_settings" + layout 'project_settings' def index respond_to do |format| @@ -66,7 +66,7 @@ class Projects::DeployKeysController < Projects::ApplicationController protected def deploy_key - @deploy_key ||= @project.deploy_keys.find(params[:id]) + @deploy_key ||= DeployKey.find(params[:id]) end def create_params diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb index f4a18a5e8f7..2e6ab7903b8 100644 --- a/app/controllers/projects/discussions_controller.rb +++ b/app/controllers/projects/discussions_controller.rb @@ -1,5 +1,5 @@ class Projects::DiscussionsController < Projects::ApplicationController - before_action :module_enabled + before_action :check_merge_requests_available! before_action :merge_request before_action :discussion before_action :authorize_resolve_discussion! @@ -34,8 +34,4 @@ class Projects::DiscussionsController < Projects::ApplicationController def authorize_resolve_discussion! access_denied! unless discussion.can_resolve?(current_user) end - - def module_enabled - render_404 unless @project.feature_available?(:merge_requests, current_user) - end end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 3869d4b2d56..f88a1ffd1e9 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -15,8 +15,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController respond_to do |format| format.html format.json do - Gitlab::PollingInterval.set_header(response, interval: 3_000) - render json: { environments: EnvironmentSerializer .new(project: @project, current_user: @current_user) diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 928f17e6a8e..7d0e2b3e2ef 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -4,7 +4,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController include ActionController::HttpAuthentication::Basic include KerberosSpnegoHelper - attr_reader :authentication_result + attr_reader :authentication_result, :redirected_path delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true @@ -14,7 +14,6 @@ class Projects::GitHttpClientController < Projects::ApplicationController skip_before_action :verify_authenticity_token skip_before_action :repository before_action :authenticate_user - before_action :ensure_project_found! private @@ -68,38 +67,14 @@ class Projects::GitHttpClientController < Projects::ApplicationController headers['Www-Authenticate'] = challenges.join("\n") if challenges.any? end - def ensure_project_found! - render_not_found if project.blank? - end - def project - return @project if defined?(@project) - - project_id, _ = project_id_with_suffix - @project = - if project_id.blank? - nil - else - Project.find_by_full_path("#{params[:namespace_id]}/#{project_id}") - end - end + parse_repo_path unless defined?(@project) - # This method returns two values so that we can parse - # params[:project_id] (untrusted input!) in exactly one place. - def project_id_with_suffix - id = params[:project_id] || '' - - %w[.wiki.git .git].each do |suffix| - if id.end_with?(suffix) - # Be careful to only remove the suffix from the end of 'id'. - # Accidentally removing it from the middle is how security - # vulnerabilities happen! - return [id.slice(0, id.length - suffix.length), suffix] - end - end + @project + end - # Something is wrong with params[:project_id]; do not pass it on. - [nil, nil] + def parse_repo_path + @project, @wiki, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}") end def render_missing_personal_token @@ -114,14 +89,9 @@ class Projects::GitHttpClientController < Projects::ApplicationController end def wiki? - return @wiki if defined?(@wiki) - - _, suffix = project_id_with_suffix - @wiki = suffix == '.wiki.git' - end + parse_repo_path unless defined?(@wiki) - def render_not_found - render plain: 'Not Found', status: :not_found + @wiki end def handle_basic_authentication(login, password) diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index b6b62da7b60..71ae60cb8cd 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -56,7 +56,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController end def access - @access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities) + @access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities, redirected_path: redirected_path) end def access_actor diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 56f76e752d0..dfc6baa34a4 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -9,7 +9,7 @@ class Projects::IssuesController < Projects::ApplicationController prepend_before_action :authenticate_user!, only: [:new] before_action :redirect_to_external_issue_tracker, only: [:index, :new] - before_action :module_enabled + before_action :check_issues_available! before_action :issue, except: [:index, :new, :create, :bulk_update] # Allow write(create) issue @@ -250,7 +250,7 @@ class Projects::IssuesController < Projects::ApplicationController return render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user) end - def module_enabled + def check_issues_available! return render_404 unless @project.feature_available?(:issues, current_user) && @project.default_issues_tracker? end diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 1beac202efe..daa973c9281 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -1,7 +1,7 @@ class Projects::LabelsController < Projects::ApplicationController include ToggleSubscriptionAction - before_action :module_enabled + before_action :check_issuables_available! before_action :label, only: [:edit, :update, :destroy, :promote] before_action :find_labels, only: [:index, :set_priorities, :remove_priority, :toggle_subscription] before_action :authorize_read_label! @@ -135,12 +135,6 @@ class Projects::LabelsController < Projects::ApplicationController protected - def module_enabled - unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user) - return render_404 - end - end - def label_params params.require(:label).permit(:title, :description, :color) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 314906b5f09..164a8824277 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -7,7 +7,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController include ToggleAwardEmoji include IssuableCollections - before_action :module_enabled + before_action :check_merge_requests_available! before_action :merge_request, only: [ :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :pipeline_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues, :commit_change_content @@ -143,8 +143,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController # Get commits from repository # or from cache if already merged @commits = @merge_request.commits - @note_counts = Note.where(commit_id: @commits.map(&:id)). - group(:commit_id).count + @note_counts = Note.where(commit_id: @commits.map(&:id)) + .group(:commit_id).count render json: { html: view_to_html_string('projects/merge_requests/show/_commits') } end @@ -192,9 +192,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController end begin - MergeRequests::Conflicts::ResolveService. - new(merge_request). - execute(current_user, params) + MergeRequests::Conflicts::ResolveService + .new(merge_request) + .execute(current_user, params) flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.' @@ -461,10 +461,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController return render_404 unless @conflicts_list.can_be_resolved_by?(current_user) end - def module_enabled - return render_404 unless @project.feature_available?(:merge_requests, current_user) - end - def validates_merge_request # Show git not found page # if there is no saved commits between source & target branch @@ -562,8 +558,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commits = @merge_request.compare_commits.reverse @commit = @merge_request.diff_head_commit - @note_counts = Note.where(commit_id: @commits.map(&:id)). - group(:commit_id).count + @note_counts = Note.where(commit_id: @commits.map(&:id)) + .group(:commit_id).count @labels = LabelsFinder.new(current_user, project_id: @project.id).execute diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index ae16f69955a..953b1e83e49 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -1,8 +1,8 @@ class Projects::MilestonesController < Projects::ApplicationController include MilestoneActions - before_action :module_enabled - before_action :milestone, only: [:edit, :update, :destroy, :show, :sort_issues, :sort_merge_requests, :merge_requests, :participants, :labels] + before_action :check_issuables_available! + before_action :milestone, only: [:edit, :update, :destroy, :show, :merge_requests, :participants, :labels] # Allow read any milestone before_action :authorize_read_milestone! @@ -85,22 +85,6 @@ class Projects::MilestonesController < Projects::ApplicationController end end - def sort_issues - @milestone.sort_issues(params['sortable_issue'].map(&:to_i)) - - render json: { saved: true } - end - - def sort_merge_requests - @merge_requests = @milestone.merge_requests.where(id: params['sortable_merge_request']) - @merge_requests.each do |merge_request| - merge_request.position = params['sortable_merge_request'].index(merge_request.id.to_s) + 1 - merge_request.save - end - - render json: { saved: true } - end - protected def milestone @@ -111,12 +95,6 @@ class Projects::MilestonesController < Projects::ApplicationController return render_404 unless can?(current_user, :admin_milestone, @project) end - def module_enabled - unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user) - return render_404 - end - end - def milestone_params params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event) end diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 6f009d61950..24fe78bc1bd 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -14,8 +14,8 @@ module Projects def define_runners_variables @project_runners = @project.runners.ordered - @assignable_runners = current_user.ci_authorized_runners. - assignable_for(project).ordered.page(params[:page]).per(20) + @assignable_runners = current_user.ci_authorized_runners + .assignable_for(project).ordered.page(params[:page]).per(20) @shared_runners = Ci::Runner.shared.active @shared_runners_count = @shared_runners.count(:all) end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 8a8f8d6a27d..98dd307bd9d 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -5,7 +5,7 @@ class Projects::SnippetsController < Projects::ApplicationController include SnippetsActions include RendersBlob - before_action :module_enabled + before_action :check_snippets_available! before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam] # Allow read any snippet @@ -102,10 +102,6 @@ class Projects::SnippetsController < Projects::ApplicationController return render_404 unless can?(current_user, :admin_project_snippet, @snippet) end - def module_enabled - return render_404 unless @project.feature_available?(:snippets, current_user) - end - def snippet_params params.require(:project_snippet).permit(:title, :content, :file_name, :private, :visibility_level, :description) end diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index afbea3e2b40..ebc9f4edab4 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -29,8 +29,8 @@ class Projects::TagsController < Projects::ApplicationController end def create - result = Tags::CreateService.new(@project, current_user). - execute(params[:tag_name], params[:ref], params[:message], params[:release_description]) + result = Tags::CreateService.new(@project, current_user) + .execute(params[:tag_name], params[:ref], params[:message], params[:release_description]) if result[:status] == :success @tag = result[:tag] diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index d7c702b94f8..0d8186dce02 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -128,8 +128,8 @@ class SessionsController < Devise::SessionsController end def log_audit_event(user, options = {}) - AuditEventService.new(user, user, options). - for_authentication.security_event + AuditEventService.new(user, user, options) + .for_authentication.security_event end def log_user_activity(user) diff --git a/app/controllers/sherlock/application_controller.rb b/app/controllers/sherlock/application_controller.rb index 682ca5e3821..6bdd3568a78 100644 --- a/app/controllers/sherlock/application_controller.rb +++ b/app/controllers/sherlock/application_controller.rb @@ -4,8 +4,8 @@ module Sherlock def find_transaction if params[:transaction_id] - @transaction = Gitlab::Sherlock.collection. - find_transaction(params[:transaction_id]) + @transaction = Gitlab::Sherlock.collection + .find_transaction(params[:transaction_id]) end end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index c211106fbaa..8131eba6a2f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -106,11 +106,11 @@ class UsersController < ApplicationController def load_events # Get user activity feed for projects common for both users - @events = user.recent_events. - merge(projects_for_current_user). - references(:project). - with_associations. - limit_recent(20, params[:offset]) + @events = user.recent_events + .merge(projects_for_current_user) + .references(:project) + .with_associations + .limit_recent(20, params[:offset]) end def load_projects diff --git a/app/finders/events_finder.rb b/app/finders/events_finder.rb index b0450ddc1fd..46ecbaba73a 100644 --- a/app/finders/events_finder.rb +++ b/app/finders/events_finder.rb @@ -33,7 +33,8 @@ class EventsFinder private def by_current_user_access(events) - events.merge(ProjectsFinder.new(current_user: current_user).execute).references(:project) + events.merge(ProjectsFinder.new(current_user: current_user).execute) + .joins(:project) end def by_action(events) diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb index fce3775f40e..067aff408df 100644 --- a/app/finders/group_members_finder.rb +++ b/app/finders/group_members_finder.rb @@ -8,9 +8,9 @@ class GroupMembersFinder return group_members unless @group.parent - parents_members = GroupMember.non_request. - where(source_id: @group.ancestors.select(:id)). - where.not(user_id: @group.users.select(:id)) + parents_members = GroupMember.non_request + .where(source_id: @group.ancestors.select(:id)) + .where.not(user_id: @group.users.select(:id)) wheres = ["members.id IN (#{group_members.select(:id).to_sql})"] wheres << "members.id IN (#{parents_members.select(:id).to_sql})" diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb index f043c38c6f9..f2d3b90b8e2 100644 --- a/app/finders/group_projects_finder.rb +++ b/app/finders/group_projects_finder.rb @@ -29,35 +29,69 @@ class GroupProjectsFinder < ProjectsFinder private def init_collection - only_owned = options.fetch(:only_owned, false) - only_shared = options.fetch(:only_shared, false) + projects = if current_user + collection_with_user + else + collection_without_user + end - projects = [] + union(projects) + end - if current_user - if group.users.include?(current_user) - projects << group.projects unless only_shared - projects << group.shared_projects unless only_owned + def collection_with_user + if group.users.include?(current_user) + if only_shared? + [shared_projects] + elsif only_owned? + [owned_projects] else - unless only_shared - projects << group.projects.visible_to_user(current_user) - projects << group.projects.public_to_user(current_user) - end - - unless only_owned - projects << group.shared_projects.visible_to_user(current_user) - projects << group.shared_projects.public_to_user(current_user) - end + [shared_projects, owned_projects] end else - projects << group.projects.public_only unless only_shared - projects << group.shared_projects.public_only unless only_owned + if only_shared? + [shared_projects.public_or_visible_to_user(current_user)] + elsif only_owned? + [owned_projects.public_or_visible_to_user(current_user)] + else + [ + owned_projects.public_or_visible_to_user(current_user), + shared_projects.public_or_visible_to_user(current_user) + ] + end end + end - projects + def collection_without_user + if only_shared? + [shared_projects.public_only] + elsif only_owned? + [owned_projects.public_only] + else + [shared_projects.public_only, owned_projects.public_only] + end end def union(items) - find_union(items, Project) + if items.one? + items.first + else + find_union(items, Project) + end + end + + def only_owned? + options.fetch(:only_owned, false) + end + + def only_shared? + options.fetch(:only_shared, false) + end + + def owned_projects + group.projects + end + + def shared_projects + group.shared_projects end end diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index f68610e197c..e6fb112e7f2 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -5,8 +5,10 @@ class GroupsFinder < UnionFinder end def execute - groups = find_union(all_groups, Group).with_route.order_id_desc - by_parent(groups) + items = all_groups.map do |item| + by_parent(item) + end + find_union(items, Group).with_route.order_id_desc end private @@ -16,12 +18,22 @@ class GroupsFinder < UnionFinder def all_groups groups = [] - groups << current_user.authorized_groups if current_user + if current_user + groups << Gitlab::GroupHierarchy.new(groups_for_ancestors, groups_for_descendants).all_groups + end groups << Group.unscoped.public_to_user(current_user) groups end + def groups_for_ancestors + current_user.authorized_groups + end + + def groups_for_descendants + current_user.groups + end + def by_parent(groups) return groups unless params[:parent] diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 957ad875858..558f8b5e2e5 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -41,6 +41,7 @@ class IssuableFinder items = by_iids(items) items = by_milestone(items) items = by_label(items) + items = by_created_at(items) # Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far items = by_project(items) @@ -402,6 +403,18 @@ class IssuableFinder params[:non_archived].present? ? items.non_archived : items end + def by_created_at(items) + if params[:created_after].present? + items = items.where(items.klass.arel_table[:created_at].gteq(params[:created_after])) + end + + if params[:created_before].present? + items = items.where(items.klass.arel_table[:created_at].lteq(params[:created_before])) + end + + items + end + def current_user_related? params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' end diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 5bf722d1ec6..8bfbe37c543 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -28,34 +28,56 @@ class ProjectsFinder < UnionFinder end def execute - items = init_collection - items = items.map do |item| - item = by_ids(item) - item = by_personal(item) - item = by_starred(item) - item = by_trending(item) - item = by_visibilty_level(item) - item = by_tags(item) - item = by_search(item) - by_archived(item) - end - items = union(items) - sort(items) + collection = init_collection + collection = by_ids(collection) + collection = by_personal(collection) + collection = by_starred(collection) + collection = by_trending(collection) + collection = by_visibilty_level(collection) + collection = by_tags(collection) + collection = by_search(collection) + collection = by_archived(collection) + + sort(collection) end private def init_collection - projects = [] + if current_user + collection_with_user + else + collection_without_user + end + end - if params[:owned].present? - projects << current_user.owned_projects if current_user + def collection_with_user + if owned_projects? + current_user.owned_projects else - projects << current_user.authorized_projects if current_user - projects << Project.unscoped.public_to_user(current_user) unless params[:non_public].present? + if private_only? + current_user.authorized_projects + else + Project.public_or_visible_to_user(current_user) + end end + end + + # Builds a collection for an anonymous user. + def collection_without_user + if private_only? || owned_projects? + Project.none + else + Project.public_to_user + end + end + + def owned_projects? + params[:owned].present? + end - projects + def private_only? + params[:non_public].present? end def by_ids(items) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 2bfc7586adc..a3b243fccb7 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -68,7 +68,7 @@ module ApplicationHelper end end - def avatar_icon(user_or_email = nil, size = nil, scale = 2) + def avatar_icon(user_or_email = nil, size = nil, scale = 2, only_path: true) user = if user_or_email.is_a?(User) user_or_email @@ -77,7 +77,7 @@ module ApplicationHelper end if user - user.avatar_url(size: size) || default_avatar + user.avatar_url(size: size, only_path: only_path) || default_avatar else gravatar_icon(user_or_email, size, scale) end @@ -167,9 +167,9 @@ module ApplicationHelper css_classes = short_format ? 'js-short-timeago' : 'js-timeago' css_classes << " #{html_class}" unless html_class.blank? - element = content_tag :time, time.strftime("%b %d, %Y"), + element = content_tag :time, l(time, format: "%b %d, %Y"), class: css_classes, - title: time.to_time.in_time_zone.to_s(:medium), + title: l(time.to_time.in_time_zone, format: :timeago_tooltip), datetime: time.to_time.getutc.iso8601, data: { toggle: 'tooltip', diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 06822747d11..16a99addd0b 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -66,12 +66,12 @@ module DiffHelper discussions_left = discussions_right = nil - if left && (left.unchanged? || left.discussable?) + if left && left.discussable? && (left.unchanged? || left.removed?) line_code = diff_file.line_code(left) discussions_left = @grouped_diff_discussions[line_code] end - if right&.discussable? + if right && right.discussable? && right.added? line_code = diff_file.line_code(right) discussions_right = @grouped_diff_discussions[line_code] end diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index 014fc46b130..8ceb5c36bda 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -8,10 +8,10 @@ module FormHelper content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do content_tag(:h4, headline) << content_tag(:ul) do - model.errors.full_messages. - map { |msg| content_tag(:li, msg) }. - join. - html_safe + model.errors.full_messages + .map { |msg| content_tag(:li, msg) } + .join + .html_safe end end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 5e8f0849969..3259a9c1933 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -138,8 +138,8 @@ module IssuablesHelper end output << " ".html_safe - output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm") - output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg") + output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "hidden-xs hidden-sm") + output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "hidden-md hidden-lg") output end @@ -216,7 +216,8 @@ module IssuablesHelper initialTitleHtml: markdown_field(issuable, :title), initialTitleText: issuable.title, initialDescriptionHtml: markdown_field(issuable, :description), - initialDescriptionText: issuable.description + initialDescriptionText: issuable.description, + initialTaskStatus: issuable.task_status } data.merge!(updated_at_by(issuable)) diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index c59d8dafc83..64ad7b280cb 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -10,8 +10,8 @@ module NotesHelper Ability.can_edit_note?(current_user, note) end - def note_supports_slash_commands?(note) - Notes::SlashCommandsService.supported?(note, current_user) + def note_supports_quick_actions?(note) + Notes::QuickActionsService.supported?(note, current_user) end def noteable_json(noteable) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index c11dd49f4a7..d10e0bd45b0 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -80,7 +80,7 @@ module ProjectsHelper end def remove_fork_project_message(project) - _("You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?") % + _("You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?") % { forked_from_project: @project.forked_from_project.name_with_namespace } end @@ -151,14 +151,21 @@ module ProjectsHelper disabled: disabled_option ) - content_tag( - :select, - options, - name: "project[project_feature_attributes][#{field}]", - id: "project_project_feature_attributes_#{field}", - class: "pull-right form-control #{repo_children_classes(field)}", - data: { field: field } - ).html_safe + content_tag :div, class: "select-wrapper" do + concat( + content_tag( + :select, + options, + name: "project[project_feature_attributes][#{field}]", + id: "project_project_feature_attributes_#{field}", + class: "pull-right form-control select-control #{repo_children_classes(field)} ", + data: { field: field } + ) + ) + concat( + icon('chevron-down') + ) + end.html_safe end def link_to_autodeploy_doc @@ -187,8 +194,8 @@ module ProjectsHelper end def load_pipeline_status(projects) - Gitlab::Cache::Ci::ProjectPipelineStatus. - load_in_batch_for_projects(projects) + Gitlab::Cache::Ci::ProjectPipelineStatus + .load_in_batch_for_projects(projects) end private diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 9c46035057f..8f15904f068 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -97,8 +97,8 @@ module SearchHelper # Autocomplete results for the current user's projects def projects_autocomplete(term, limit = 5) - current_user.authorized_projects.search_by_title(term). - sorted_by_stars.non_archived.limit(limit).map do |p| + current_user.authorized_projects.search_by_title(term) + .sorted_by_stars.non_archived.limit(limit).map do |p| { category: "Projects", id: p.id, diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb index 3e3f6246fc5..99212a3438f 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -6,8 +6,8 @@ module WikiHelper # Returns a String composed of the capitalized name of each directory and the # capitalized name of the page itself. def breadcrumb(page_slug) - page_slug.split('/'). - map { |dir_or_page| WikiPage.unhyphenize(dir_or_page).capitalize }. - join(' / ') + page_slug.split('/') + .map { |dir_or_page| WikiPage.unhyphenize(dir_or_page).capitalize } + .join(' / ') end end diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index ebe60441603..91b62dabbcd 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -19,9 +19,9 @@ class AwardEmoji < ActiveRecord::Base class << self def votes_for_collection(ids, type) - select('name', 'awardable_id', 'COUNT(*) as count'). - where('name IN (?) AND awardable_type = ? AND awardable_id IN (?)', [DOWNVOTE_NAME, UPVOTE_NAME], type, ids). - group('name', 'awardable_id') + select('name', 'awardable_id', 'COUNT(*) as count') + .where('name IN (?) AND awardable_type = ? AND awardable_id IN (?)', [DOWNVOTE_NAME, UPVOTE_NAME], type, ids) + .group('name', 'awardable_id') end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 58758f7ca8a..a300536532b 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -138,17 +138,6 @@ module Ci ExpandVariables.expand(environment, simple_variables) if environment end - def environment_url - return @environment_url if defined?(@environment_url) - - @environment_url = - if unexpanded_url = options&.dig(:environment, :url) - ExpandVariables.expand(unexpanded_url, simple_variables) - else - persisted_environment&.external_url - end - end - def has_environment? environment.present? end @@ -192,7 +181,7 @@ module Ci slugified.gsub(/[^a-z0-9]/, '-')[0..62] end - # Variables whose value does not depend on other variables + # Variables whose value does not depend on environment def simple_variables variables = predefined_variables variables += project.predefined_variables @@ -207,7 +196,8 @@ module Ci variables end - # All variables, including those dependent on other variables + # All variables, including those dependent on environment, which could + # contain unexpanded variables. def variables simple_variables.concat(persisted_environment_variables) end @@ -481,9 +471,10 @@ module Ci variables = persisted_environment.predefined_variables - if url = environment_url - variables << { key: 'CI_ENVIRONMENT_URL', value: url, public: true } - end + # Here we're passing unexpanded environment_url for runner to expand, + # and we need to make sure that CI_ENVIRONMENT_NAME and + # CI_ENVIRONMENT_SLUG so on are available for the URL be expanded. + variables << { key: 'CI_ENVIRONMENT_URL', value: environment_url, public: true } if environment_url variables end @@ -506,6 +497,10 @@ module Ci variables end + def environment_url + options&.dig(:environment, :url) || persisted_environment&.external_url + end + def build_attributes_from_config return {} unless pipeline.config_processor diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 9ddecba5183..1b3e5a25ac2 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -168,8 +168,8 @@ module Ci end def stages_names - statuses.order(:stage_idx).distinct. - pluck(:stage, :stage_idx).map(&:first) + statuses.order(:stage_idx).distinct + .pluck(:stage, :stage_idx).map(&:first) end def legacy_stage(name) diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 487ba61bc9c..d12f96f3d0b 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -30,8 +30,8 @@ module Ci scope :assignable_for, ->(project) do # FIXME: That `to_sql` is needed to workaround a weird Rails bug. # Without that, placeholders would miss one and couldn't match. - where(locked: false). - where.not("id IN (#{project.runners.select(:id).to_sql})").specific + where(locked: false) + .where.not("id IN (#{project.runners.select(:id).to_sql})").specific end validate :tag_constraints diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index ea10d004c9c..d178ee4422b 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -67,7 +67,6 @@ module Issuable scope :authored, ->(user) { where(author_id: user) } scope :recent, -> { reorder(id: :desc) } - scope :order_position_asc, -> { reorder(position: :asc) } scope :of_projects, ->(ids) { where(project_id: ids) } scope :of_milestones, ->(ids) { where(milestone_id: ids) } scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) } @@ -139,7 +138,6 @@ module Issuable when 'upvotes_desc' then order_upvotes_desc when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels) when 'priority' then order_due_date_and_labels_priority(excluded_labels: excluded_labels) - when 'position_asc' then order_position_asc else order_by(method) end @@ -163,9 +161,9 @@ module Issuable # milestones_due_date = 'MIN(milestones.due_date)' - order_milestone_due_asc. - order_labels_priority(excluded_labels: excluded_labels, extra_select_columns: [milestones_due_date]). - reorder(Gitlab::Database.nulls_last_order(milestones_due_date, 'ASC'), + order_milestone_due_asc + .order_labels_priority(excluded_labels: excluded_labels, extra_select_columns: [milestones_due_date]) + .reorder(Gitlab::Database.nulls_last_order(milestones_due_date, 'ASC'), Gitlab::Database.nulls_last_order('highest_priority', 'ASC')) end @@ -184,9 +182,9 @@ module Issuable "(#{highest_priority}) AS highest_priority" ] + extra_select_columns - select(select_columns.join(', ')). - group(arel_table[:id]). - reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')) + select(select_columns.join(', ')) + .group(arel_table[:id]) + .reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')) end def with_label(title, sort = nil) diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index a3472af5c55..01599ce49c6 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -40,10 +40,18 @@ module Milestoneish def issues_visible_to_user(user) memoize_per_user(user, :issues_visible_to_user) do IssuesFinder.new(user, issues_finder_params) - .execute.includes(:assignees).where(milestone_id: milestoneish_ids) + .execute.preload(:assignees).where(milestone_id: milestoneish_ids) end end + def sorted_issues(user) + issues_visible_to_user(user).preload_associations.sort('label_priority') + end + + def sorted_merge_requests + merge_requests.sort('label_priority') + end + def upcoming? start_date && start_date.future? end diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index f1d8532a6d6..7cb9a28a284 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -18,10 +18,10 @@ module RelativePositioning prev_pos = nil if self.relative_position - prev_pos = self.class. - in_projects(project.id). - where('relative_position < ?', self.relative_position). - maximum(:relative_position) + prev_pos = self.class + .in_projects(project.id) + .where('relative_position < ?', self.relative_position) + .maximum(:relative_position) end prev_pos @@ -31,10 +31,10 @@ module RelativePositioning next_pos = nil if self.relative_position - next_pos = self.class. - in_projects(project.id). - where('relative_position > ?', self.relative_position). - minimum(:relative_position) + next_pos = self.class + .in_projects(project.id) + .where('relative_position > ?', self.relative_position) + .minimum(:relative_position) end next_pos diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index 63d02b76f6b..ec7796a9dbb 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -107,6 +107,14 @@ module Routable RequestStore[key] ||= uncached_full_path end + def build_full_path + if parent && path + parent.full_path + '/' + path + else + path + end + end + private def uncached_full_path @@ -135,14 +143,6 @@ module Routable end end - def build_full_path - if parent && path - parent.full_path + '/' + path - else - path - end - end - def update_route prepare_route route.save diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index b9a2d812edd..a155a064032 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -39,12 +39,12 @@ module Sortable private def highest_label_priority(target_type_column: nil, target_type: nil, target_column:, project_column:, excluded_labels: []) - query = Label.select(LabelPriority.arel_table[:priority].minimum). - left_join_priorities. - joins(:label_links). - where("label_priorities.project_id = #{project_column}"). - where("label_links.target_id = #{target_column}"). - reorder(nil) + query = Label.select(LabelPriority.arel_table[:priority].minimum) + .left_join_priorities + .joins(:label_links) + .where("label_priorities.project_id = #{project_column}") + .where("label_links.target_id = #{target_column}") + .reorder(nil) query = if target_type_column diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb index 83daa9b1a64..f60a0f8f438 100644 --- a/app/models/concerns/subscribable.rb +++ b/app/models/concerns/subscribable.rb @@ -27,16 +27,16 @@ module Subscribable end def subscribers(project) - subscriptions_available(project). - where(subscribed: true). - map(&:user) + subscriptions_available(project) + .where(subscribed: true) + .map(&:user) end def toggle_subscription(user, project = nil) unsubscribe_from_other_levels(user, project) - find_or_initialize_subscription(user, project). - update(subscribed: !subscribed?(user, project)) + find_or_initialize_subscription(user, project) + .update(subscribed: !subscribed?(user, project)) end def subscribe(user, project = nil) @@ -69,14 +69,14 @@ module Subscribable end def find_or_initialize_subscription(user, project) - subscriptions. - find_or_initialize_by(user_id: user.id, project_id: project.try(:id)) + subscriptions + .find_or_initialize_by(user_id: user.id, project_id: project.try(:id)) end def subscriptions_available(project) t = Subscription.arel_table - subscriptions. - where(t[:project_id].eq(nil).or(t[:project_id].eq(project.try(:id)))) + subscriptions + .where(t[:project_id].eq(nil).or(t[:project_id].eq(project.try(:id)))) end end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 00bf0c118ae..056c49e7162 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -58,10 +58,10 @@ class Deployment < ActiveRecord::Base def update_merge_request_metrics! return unless environment.update_merge_request_metrics? - merge_requests = project.merge_requests. - joins(:metrics). - where(target_branch: self.ref, merge_request_metrics: { first_deployed_to_production_at: nil }). - where("merge_request_metrics.merged_at <= ?", self.created_at) + merge_requests = project.merge_requests + .joins(:metrics) + .where(target_branch: self.ref, merge_request_metrics: { first_deployed_to_production_at: nil }) + .where("merge_request_metrics.merged_at <= ?", self.created_at) if previous_deployment merge_requests = merge_requests.where("merge_request_metrics.merged_at >= ?", previous_deployment.created_at) @@ -76,17 +76,17 @@ class Deployment < ActiveRecord::Base merge_requests.map(&:id) end - MergeRequest::Metrics. - where(merge_request_id: merge_request_ids, first_deployed_to_production_at: nil). - update_all(first_deployed_to_production_at: self.created_at) + MergeRequest::Metrics + .where(merge_request_id: merge_request_ids, first_deployed_to_production_at: nil) + .update_all(first_deployed_to_production_at: self.created_at) end def previous_deployment @previous_deployment ||= - project.deployments.joins(:environment). - where(environments: { name: self.environment.name }, ref: self.ref). - where.not(id: self.id). - take + project.deployments.joins(:environment) + .where(environments: { name: self.environment.name }, ref: self.ref) + .where.not(id: self.id) + .take end def stop_action diff --git a/app/models/environment.rb b/app/models/environment.rb index b391a487b30..7ad36f1d80c 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -40,9 +40,9 @@ class Environment < ActiveRecord::Base scope :stopped, -> { with_state(:stopped) } scope :order_by_last_deployed_at, -> do max_deployment_id_sql = - Deployment.select(Deployment.arel_table[:id].maximum). - where(Deployment.arel_table[:environment_id].eq(arel_table[:id])). - to_sql + Deployment.select(Deployment.arel_table[:id].maximum) + .where(Deployment.arel_table[:environment_id].eq(arel_table[:id])) + .to_sql order(Gitlab::Database.nulls_first_order("(#{max_deployment_id_sql})", 'ASC')) end diff --git a/app/models/event.rb b/app/models/event.rb index fad6ff03927..29bc141c5cd 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -376,9 +376,9 @@ class Event < ActiveRecord::Base # At this point it's possible for multiple threads/processes to try to # update the project. Only one query should actually perform the update, # hence we add the extra WHERE clause for last_activity_at. - Project.unscoped.where(id: project_id). - where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago). - update_all(last_activity_at: created_at) + Project.unscoped.where(id: project_id) + .where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago) + .update_all(last_activity_at: created_at) end def authored_by?(user) @@ -392,7 +392,7 @@ class Event < ActiveRecord::Base end def set_last_repository_updated_at - Project.unscoped.where(id: project_id). - update_all(last_repository_updated_at: created_at) + Project.unscoped.where(id: project_id) + .update_all(last_repository_updated_at: created_at) end end diff --git a/app/models/group.rb b/app/models/group.rb index 5bb2cdc5eff..0b93460d473 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -206,8 +206,8 @@ class Group < Namespace end def refresh_members_authorized_projects - UserProjectAccessChangedService.new(user_ids_for_project_authorizations). - execute + UserProjectAccessChangedService.new(user_ids_for_project_authorizations) + .execute end def user_ids_for_project_authorizations @@ -225,10 +225,10 @@ class Group < Namespace def max_member_access_for_user(user) return GroupMember::OWNER if user.admin? - members_with_parents. - where(user_id: user). - reorder(access_level: :desc). - first&. + members_with_parents + .where(user_id: user) + .reorder(access_level: :desc) + .first&. access_level || GroupMember::NO_ACCESS end diff --git a/app/models/issue.rb b/app/models/issue.rb index 693cc21bb40..3a9a6dba601 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -9,6 +9,9 @@ class Issue < ActiveRecord::Base include Spammable include FasterCacheKeys include RelativePositioning + include IgnorableColumn + + ignore_column :position DueDateStruct = Struct.new(:title, :name).freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze @@ -44,7 +47,7 @@ class Issue < ActiveRecord::Base scope :created_after, -> (datetime) { where("created_at >= ?", datetime) } - scope :include_associations, -> { includes(:labels, project: :namespace) } + scope :preload_associations, -> { preload(:labels, project: :namespace) } after_save :expire_etag_cache @@ -121,8 +124,8 @@ class Issue < ActiveRecord::Base end def self.order_by_position_and_priority - order_labels_priority. - reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC'), + order_labels_priority + .reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC'), Gitlab::Database.nulls_last_order('highest_priority', 'ASC'), "id DESC") end diff --git a/app/models/issue_collection.rb b/app/models/issue_collection.rb index f0b7d9914c8..49f011c113f 100644 --- a/app/models/issue_collection.rb +++ b/app/models/issue_collection.rb @@ -17,9 +17,9 @@ class IssueCollection # Given all the issue projects we get a list of projects that the current # user has at least reporter access to. - projects_with_reporter_access = user. - projects_with_reporter_access_limited_to(project_ids). - pluck(:id) + projects_with_reporter_access = user + .projects_with_reporter_access_limited_to(project_ids) + .pluck(:id) collection.select do |issue| if projects_with_reporter_access.include?(issue.project_id) diff --git a/app/models/label.rb b/app/models/label.rb index 955d6b4079b..ed6a8411da9 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -46,9 +46,9 @@ class Label < ActiveRecord::Base labels = Label.arel_table priorities = LabelPriority.arel_table - label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin). - on(labels[:id].eq(priorities[:label_id]).and(priorities[:project_id].eq(project.id))). - join_sources + label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin) + .on(labels[:id].eq(priorities[:label_id]).and(priorities[:project_id].eq(project.id))) + .join_sources joins(label_priorities).where(priorities[:priority].eq(nil)) end @@ -57,9 +57,9 @@ class Label < ActiveRecord::Base labels = Label.arel_table priorities = LabelPriority.arel_table - label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin). - on(labels[:id].eq(priorities[:label_id])). - join_sources + label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin) + .on(labels[:id].eq(priorities[:label_id])) + .join_sources joins(label_priorities) end diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb index 7126de2d488..2d5909ab25e 100644 --- a/app/models/legacy_diff_note.rb +++ b/app/models/legacy_diff_note.rb @@ -42,7 +42,7 @@ class LegacyDiffNote < Note end def for_line?(line) - !line.meta? && diff_file.line_code(line) == self.line_code + line.discussable? && diff_file.line_code(line) == self.line_code end def original_line_code diff --git a/app/models/member.rb b/app/models/member.rb index 788a32dd8e3..dc9247bc9a0 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -99,9 +99,9 @@ class Member < ActiveRecord::Base users = User.arel_table members = Member.arel_table - member_users = members.join(users, Arel::Nodes::OuterJoin). - on(members[:user_id].eq(users[:id])). - join_sources + member_users = members.join(users, Arel::Nodes::OuterJoin) + .on(members[:user_id].eq(users[:id])) + .join_sources joins(member_users) end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index dd155252ad5..f581a25f093 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -4,6 +4,9 @@ class MergeRequest < ActiveRecord::Base include Noteable include Referable include Sortable + include IgnorableColumn + + ignore_column :position belongs_to :target_project, class_name: "Project" belongs_to :source_project, class_name: "Project" @@ -574,8 +577,8 @@ class MergeRequest < ActiveRecord::Base messages = [title, description] messages.concat(commits.map(&:safe_message)) if merge_request_diff - Gitlab::ClosingIssueExtractor.new(project, current_user). - closed_by_message(messages.join("\n")) + Gitlab::ClosingIssueExtractor.new(project, current_user) + .closed_by_message(messages.join("\n")) else [] end @@ -889,7 +892,7 @@ class MergeRequest < ActiveRecord::Base !has_commits? end - def mergeable_with_slash_command?(current_user, autocomplete_precheck: false, last_diff_sha: nil) + def mergeable_with_quick_action?(current_user, autocomplete_precheck: false, last_diff_sha: nil) return false unless can_be_merged_by?(current_user) return true if autocomplete_precheck diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 99dd2130188..f1ee4d3f7a9 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -10,6 +10,7 @@ class MergeRequestDiff < ActiveRecord::Base VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta].freeze belongs_to :merge_request + has_many :merge_request_diff_files, -> { order(:merge_request_diff_id, :relative_order) } serialize :st_commits # rubocop:disable Cop/ActiverecordSerialize serialize :st_diffs # rubocop:disable Cop/ActiverecordSerialize @@ -91,7 +92,7 @@ class MergeRequestDiff < ActiveRecord::Base head_commit_sha).diffs(options) else @raw_diffs ||= {} - @raw_diffs[options] ||= load_diffs(st_diffs, options) + @raw_diffs[options] ||= load_diffs(options) end end @@ -253,24 +254,44 @@ class MergeRequestDiff < ActiveRecord::Base update_columns_serialized(new_attributes) end - def dump_diffs(diffs) - if diffs.respond_to?(:map) - diffs.map(&:to_hash) + def create_merge_request_diff_files(diffs) + rows = diffs.map.with_index do |diff, index| + diff.to_hash.merge( + merge_request_diff_id: self.id, + relative_order: index + ) end + + Gitlab::Database.bulk_insert('merge_request_diff_files', rows) end - def load_diffs(raw, options) - if valid_raw_diff?(raw) - if paths = options[:paths] - raw = raw.select do |diff| - paths.include?(diff[:old_path]) || paths.include?(diff[:new_path]) - end - end + def load_diffs(options) + return Gitlab::Git::DiffCollection.new([]) unless diffs_from_database - Gitlab::Git::DiffCollection.new(raw, options) - else - Gitlab::Git::DiffCollection.new([]) + raw = diffs_from_database + + if paths = options[:paths] + raw = raw.select do |diff| + paths.include?(diff[:old_path]) || paths.include?(diff[:new_path]) + end end + + Gitlab::Git::DiffCollection.new(raw, options) + end + + def diffs_from_database + return @diffs_from_database if defined?(@diffs_from_database) + + @diffs_from_database = + if st_diffs.present? + if valid_raw_diff?(st_diffs) + st_diffs + end + elsif merge_request_diff_files.present? + merge_request_diff_files + .as_json(only: Gitlab::Git::Diff::SERIALIZE_KEYS) + .map(&:with_indifferent_access) + end end # Load diffs between branches related to current merge request diff from repo @@ -285,11 +306,10 @@ class MergeRequestDiff < ActiveRecord::Base new_attributes[:real_size] = diff_collection.real_size if diff_collection.any? - new_diffs = dump_diffs(diff_collection) new_attributes[:state] = :collected - end - new_attributes[:st_diffs] = new_diffs || [] + create_merge_request_diff_files(diff_collection) + end # Set our state to 'overflow' to make the #empty? and #collected? # methods (generated by StateMachine) return false. diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb new file mode 100644 index 00000000000..598ebd4d829 --- /dev/null +++ b/app/models/merge_request_diff_file.rb @@ -0,0 +1,11 @@ +class MergeRequestDiffFile < ActiveRecord::Base + include Gitlab::EncodingHelper + + belongs_to :merge_request_diff + + def utf8_diff + return '' if diff.blank? + + encode_utf8(diff) if diff.respond_to?(:encoding) + end +end diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb index daafb137be4..7f7c114803d 100644 --- a/app/models/merge_requests_closing_issues.rb +++ b/app/models/merge_requests_closing_issues.rb @@ -7,9 +7,9 @@ class MergeRequestsClosingIssues < ActiveRecord::Base class << self def count_for_collection(ids) - group(:issue_id). - where(issue_id: ids). - pluck('issue_id', 'COUNT(*) as count') + group(:issue_id) + .where(issue_id: ids) + .pluck('issue_id', 'COUNT(*) as count') end end end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index b04bed4c014..d2e2749f70d 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -98,11 +98,11 @@ class Milestone < ActiveRecord::Base if Gitlab::Database.postgresql? rel.order(:project_id, :due_date).select('DISTINCT ON (project_id) id') else - rel. - group(:project_id). - having('due_date = MIN(due_date)'). - pluck(:id, :project_id, :due_date). - map(&:first) + rel + .group(:project_id) + .having('due_date = MIN(due_date)') + .pluck(:id, :project_id, :due_date) + .map(&:first) end end @@ -164,38 +164,6 @@ class Milestone < ActiveRecord::Base write_attribute(:title, sanitize_title(value)) if value.present? end - # Sorts the issues for the given IDs. - # - # This method runs a single SQL query using a CASE statement to update the - # position of all issues in the current milestone (scoped to the list of IDs). - # - # Given the ids [10, 20, 30] this method produces a SQL query something like - # the following: - # - # UPDATE issues - # SET position = CASE - # WHEN id = 10 THEN 1 - # WHEN id = 20 THEN 2 - # WHEN id = 30 THEN 3 - # ELSE position - # END - # WHERE id IN (10, 20, 30); - # - # This method expects that the IDs given in `ids` are already Fixnums. - def sort_issues(ids) - pairs = [] - - ids.each_with_index do |id, index| - pairs << id - pairs << index + 1 - end - - conditions = 'WHEN id = ? THEN ? ' * ids.length - - issues.where(id: ids). - update_all(["position = CASE #{conditions} ELSE position END", *pairs]) - end - private def milestone_format_reference(format = :iid) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index b48d73dcae7..583d4fb5244 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -181,16 +181,16 @@ class Namespace < ActiveRecord::Base def ancestors return self.class.none unless parent_id - Gitlab::GroupHierarchy. - new(self.class.where(id: parent_id)). - base_and_ancestors + Gitlab::GroupHierarchy + .new(self.class.where(id: parent_id)) + .base_and_ancestors end # Returns all the descendants of the current namespace. def descendants - Gitlab::GroupHierarchy. - new(self.class.where(parent_id: id)). - base_and_descendants + Gitlab::GroupHierarchy + .new(self.class.where(parent_id: id)) + .base_and_descendants end def user_ids_for_project_authorizations @@ -253,10 +253,10 @@ class Namespace < ActiveRecord::Base end def refresh_access_of_projects_invited_groups - Group. - joins(project_group_links: :project). - where(projects: { namespace_id: id }). - find_each(&:refresh_members_authorized_projects) + Group + .joins(project_group_links: :project) + .where(projects: { namespace_id: id }) + .find_each(&:refresh_members_authorized_projects) end def remove_exports! diff --git a/app/models/note.rb b/app/models/note.rb index 244bf169c29..ca6999427c0 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -32,7 +32,7 @@ class Note < ActiveRecord::Base # Banzai::ObjectRenderer attr_accessor :user_visible_reference_count - # Attribute used to store the attributes that have ben changed by slash commands. + # Attribute used to store the attributes that have ben changed by quick actions. attr_accessor :commands_changes default_value_for :system, false @@ -137,9 +137,9 @@ class Note < ActiveRecord::Base end def count_for_collection(ids, type) - user.select('noteable_id', 'COUNT(*) as count'). - group(:noteable_id). - where(noteable_type: type, noteable_id: ids) + user.select('noteable_id', 'COUNT(*) as count') + .group(:noteable_id) + .where(noteable_type: type, noteable_id: ids) end end diff --git a/app/models/project.rb b/app/models/project.rb index 4c394646787..2c2685875f8 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -244,8 +244,8 @@ class Project < ActiveRecord::Base scope :inside_path, ->(path) do # We need routes alias rs for JOIN so it does not conflict with # includes(:route) which we use in ProjectsFinder. - joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'"). - where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%") + joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'") + .where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%") end # "enabled" here means "not disabled". It includes private features! @@ -266,20 +266,49 @@ class Project < ActiveRecord::Base enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } + # Returns a collection of projects that is either public or visible to the + # logged in user. + def self.public_or_visible_to_user(user = nil) + if user + authorized = user + .project_authorizations + .select(1) + .where('project_authorizations.project_id = projects.id') + + levels = Gitlab::VisibilityLevel.levels_for_user(user) + + where('EXISTS (?) OR projects.visibility_level IN (?)', authorized, levels) + else + public_to_user + end + end + # project features may be "disabled", "internal" or "enabled". If "internal", # they are only available to team members. This scope returns projects where # the feature is either enabled, or internal with permission for the user. + # + # This method uses an optimised version of `with_feature_access_level` for + # logged in users to more efficiently get private projects with the given + # feature. def self.with_feature_available_for_user(feature, user) - return with_feature_enabled(feature) if user.try(:admin?) + visible = [nil, ProjectFeature::ENABLED] - unconditional = with_feature_access_level(feature, [nil, ProjectFeature::ENABLED]) - return unconditional if user.nil? + if user&.admin? + with_feature_enabled(feature) + elsif user + column = ProjectFeature.quoted_access_level_column(feature) - conditional = with_feature_access_level(feature, ProjectFeature::PRIVATE) - authorized = user.authorized_projects.merge(conditional.reorder(nil)) + authorized = user.project_authorizations.select(1) + .where('project_authorizations.project_id = projects.id') - union = Gitlab::SQL::Union.new([unconditional.select(:id), authorized.select(:id)]) - where(arel_table[:id].in(Arel::Nodes::SqlLiteral.new(union.to_sql))) + with_project_feature + .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))", + visible, + ProjectFeature::PRIVATE, + authorized) + else + with_feature_access_level(feature, visible) + end end scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') } @@ -340,14 +369,14 @@ class Project < ActiveRecord::Base # unscoping unnecessary conditions that'll be applied # when executing `where("projects.id IN (#{union.to_sql})")` projects = unscoped.select(:id).where( - ptable[:path].matches(pattern). - or(ptable[:name].matches(pattern)). - or(ptable[:description].matches(pattern)) + ptable[:path].matches(pattern) + .or(ptable[:name].matches(pattern)) + .or(ptable[:description].matches(pattern)) ) - namespaces = unscoped.select(:id). - joins(:namespace). - where(ntable[:name].matches(pattern)) + namespaces = unscoped.select(:id) + .joins(:namespace) + .where(ntable[:name].matches(pattern)) union = Gitlab::SQL::Union.new([projects, namespaces]) @@ -388,8 +417,8 @@ class Project < ActiveRecord::Base end def trending - joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id'). - reorder('trending_projects.id ASC') + joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id') + .reorder('trending_projects.id ASC') end def cached_count diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb index def09675253..73302207e6b 100644 --- a/app/models/project_authorization.rb +++ b/app/models/project_authorization.rb @@ -7,9 +7,9 @@ class ProjectAuthorization < ActiveRecord::Base validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true def self.select_from_union(union) - select(['project_id', 'MAX(access_level) AS access_level']). - from("(#{union.to_sql}) #{ProjectAuthorization.table_name}"). - group(:project_id) + select(['project_id', 'MAX(access_level) AS access_level']) + .from("(#{union.to_sql}) #{ProjectAuthorization.table_name}") + .group(:project_id) end def self.insert_authorizations(rows, per_batch = 1000) diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index e3ef4919b28..dde2a11440d 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -27,6 +27,13 @@ class ProjectFeature < ActiveRecord::Base "#{feature}_access_level".to_sym end + + def quoted_access_level_column(feature) + attribute = connection.quote_column_name(access_level_attribute(feature)) + table = connection.quote_table_name(table_name) + + "#{table}.#{attribute}" + end end # Default scopes force us to unscope here since a service may need to check diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb index 3edc395033c..d63d4ec2b12 100644 --- a/app/models/project_services/chat_message/pipeline_message.rb +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -70,7 +70,7 @@ module ChatMessage end def branch_link - "`[#{ref}](#{branch_url})`" + "[#{ref}](#{branch_url})" end def project_link diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb index 04a59d559ca..c52dd6ef8ef 100644 --- a/app/models/project_services/chat_message/push_message.rb +++ b/app/models/project_services/chat_message/push_message.rb @@ -61,7 +61,7 @@ module ChatMessage end def removed_branch_message - "#{user_name} removed #{ref_type} `#{ref}` from #{project_link}" + "#{user_name} removed #{ref_type} #{ref} from #{project_link}" end def push_message @@ -102,7 +102,7 @@ module ChatMessage end def branch_link - "`[#{ref}](#{branch_url})`" + "[#{ref}](#{branch_url})" end def project_link diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 56f42d63b2d..4d2037286a2 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -1,4 +1,4 @@ -class MattermostSlashCommandsService < ChatSlashCommandsService +class MattermostSlashCommandsService < SlashCommandsService include TriggersHelper prop_accessor :token @@ -20,8 +20,8 @@ class MattermostSlashCommandsService < ChatSlashCommandsService end def configure(user, params) - token = Mattermost::Command.new(user). - create(command(params)) + token = Mattermost::Command.new(user) + .create(command(params)) update(active: true, token: token) if token rescue Mattermost::Error => e diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb index 2182c1c7e4b..1c3892a3f75 100644 --- a/app/models/project_services/slack_slash_commands_service.rb +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -1,4 +1,4 @@ -class SlackSlashCommandsService < ChatSlashCommandsService +class SlackSlashCommandsService < SlashCommandsService include TriggersHelper def title diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb index 8b5bc24fd3c..4592cb747a0 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/slash_commands_service.rb @@ -1,6 +1,6 @@ # Base class for Chat services # This class is not meant to be used directly, but only to inherrit from. -class ChatSlashCommandsService < Service +class SlashCommandsService < Service default_value_for :category, 'chat' prop_accessor :token @@ -33,10 +33,10 @@ class ChatSlashCommandsService < Service user = find_chat_user(params) if user - Gitlab::ChatCommands::Command.new(project, user, params).execute + Gitlab::SlashCommands::Command.new(project, user, params).execute else url = authorize_chat_name_url(params) - Gitlab::ChatCommands::Presenters::Access.new(url).authorize + Gitlab::SlashCommands::Presenters::Access.new(url).authorize end end diff --git a/app/models/project_team.rb b/app/models/project_team.rb index e1cc56551ba..674eacd28e8 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -172,10 +172,10 @@ class ProjectTeam return access if user_ids.empty? - users_access = project.project_authorizations. - where(user: user_ids). - group(:user_id). - maximum(:access_level) + users_access = project.project_authorizations + .where(user: user_ids) + .group(:user_id) + .maximum(:access_level) access.merge!(users_access) diff --git a/app/models/repository.rb b/app/models/repository.rb index 7460515fea8..c67475357d9 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -241,11 +241,11 @@ class Repository cache.fetch(:"diverging_commit_counts_#{branch.name}") do # Rugged seems to throw a `ReferenceError` when given branch_names rather # than SHA-1 hashes - number_commits_behind = raw_repository. - count_commits_between(branch.dereferenced_target.sha, root_ref_hash) + number_commits_behind = raw_repository + .count_commits_between(branch.dereferenced_target.sha, root_ref_hash) - number_commits_ahead = raw_repository. - count_commits_between(root_ref_hash, branch.dereferenced_target.sha) + number_commits_ahead = raw_repository + .count_commits_between(root_ref_hash, branch.dereferenced_target.sha) { behind: number_commits_behind, ahead: number_commits_ahead } end diff --git a/app/models/todo.rb b/app/models/todo.rb index 696d139af74..7af54b2beb2 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -70,9 +70,9 @@ class Todo < ActiveRecord::Base highest_priority = highest_label_priority(params).to_sql - select("#{table_name}.*, (#{highest_priority}) AS highest_priority"). - order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')). - order('todos.created_at') + select("#{table_name}.*, (#{highest_priority}) AS highest_priority") + .order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')) + .order('todos.created_at') end end diff --git a/app/models/user.rb b/app/models/user.rb index 5d128e4b390..954a30155f7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -139,21 +139,21 @@ class User < ActiveRecord::Base presence: true, uniqueness: { case_sensitive: false } - validate :namespace_uniq, if: ->(user) { user.username_changed? } + validate :namespace_uniq, if: :username_changed? validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } - validate :unique_email, if: ->(user) { user.email_changed? } - validate :owns_notification_email, if: ->(user) { user.notification_email_changed? } - validate :owns_public_email, if: ->(user) { user.public_email_changed? } + validate :unique_email, if: :email_changed? + validate :owns_notification_email, if: :notification_email_changed? + validate :owns_public_email, if: :public_email_changed? validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } before_validation :sanitize_attrs - before_validation :set_notification_email, if: ->(user) { user.email_changed? } - before_validation :set_public_email, if: ->(user) { user.public_email_changed? } + before_validation :set_notification_email, if: :email_changed? + before_validation :set_public_email, if: :public_email_changed? - after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? } + after_update :update_emails_with_primary_email, if: :email_changed? before_save :ensure_authentication_token, :ensure_incoming_email_token - before_save :ensure_external_user_rights + before_save :ensure_user_rights_and_limits, if: :external_changed? after_save :ensure_namespace_correct after_initialize :set_projects_limit after_destroy :post_destroy_hook @@ -223,13 +223,13 @@ class User < ActiveRecord::Base scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'ASC')) } def self.with_two_factor - joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id"). - where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id]) + joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id") + .where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id]) end def self.without_two_factor - joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id"). - where("u2f.id IS NULL AND otp_required_for_login = ?", false) + joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id") + .where("u2f.id IS NULL AND otp_required_for_login = ?", false) end # @@ -300,9 +300,9 @@ class User < ActiveRecord::Base pattern = "%#{query}%" where( - table[:name].matches(pattern). - or(table[:email].matches(pattern)). - or(table[:username].matches(pattern)) + table[:name].matches(pattern) + .or(table[:email].matches(pattern)) + .or(table[:username].matches(pattern)) ) end @@ -317,10 +317,10 @@ class User < ActiveRecord::Base matched_by_emails_user_ids = email_table.project(email_table[:user_id]).where(email_table[:email].matches(pattern)) where( - table[:name].matches(pattern). - or(table[:email].matches(pattern)). - or(table[:username].matches(pattern)). - or(table[:id].in(matched_by_emails_user_ids)) + table[:name].matches(pattern) + .or(table[:email].matches(pattern)) + .or(table[:username].matches(pattern)) + .or(table[:id].in(matched_by_emails_user_ids)) ) end @@ -503,8 +503,8 @@ class User < ActiveRecord::Base # Returns the groups a user has access to def authorized_groups - union = Gitlab::SQL::Union. - new([groups.select(:id), authorized_projects.select(:namespace_id)]) + union = Gitlab::SQL::Union + .new([groups.select(:id), authorized_projects.select(:namespace_id)]) Group.where("namespaces.id IN (#{union.to_sql})") end @@ -533,8 +533,8 @@ class User < ActiveRecord::Base projects = super() if min_access_level - projects = projects. - where('project_authorizations.access_level >= ?', min_access_level) + projects = projects + .where('project_authorizations.access_level >= ?', min_access_level) end projects @@ -619,9 +619,9 @@ class User < ActiveRecord::Base next unless project if project.repository.branch_exists?(event.branch_name) - merge_requests = MergeRequest.where("created_at >= ?", event.created_at). - where(source_project_id: project.id, - source_branch: event.branch_name) + merge_requests = MergeRequest.where("created_at >= ?", event.created_at) + .where(source_project_id: project.id, + source_branch: event.branch_name) merge_requests.empty? end end @@ -832,8 +832,8 @@ class User < ActiveRecord::Base def toggle_star(project) UsersStarProject.transaction do - user_star_project = users_star_projects. - where(project: project, user: self).lock(true).first + user_star_project = users_star_projects + .where(project: project, user: self).lock(true).first if user_star_project user_star_project.destroy @@ -869,11 +869,11 @@ class User < ActiveRecord::Base # ms on a database with a similar size to GitLab.com's database. On the other # hand, using a subquery means we can get the exact same data in about 40 ms. def contributed_projects - events = Event.select(:project_id). - contributions.where(author_id: self). - where("created_at > ?", Time.now - 1.year). - uniq. - reorder(nil) + events = Event.select(:project_id) + .contributions.where(author_id: self) + .where("created_at > ?", Time.now - 1.year) + .uniq + .reorder(nil) Project.where(id: events) end @@ -884,9 +884,9 @@ class User < ActiveRecord::Base def ci_authorized_runners @ci_authorized_runners ||= begin - runner_ids = Ci::RunnerProject. - where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})"). - select(:runner_id) + runner_ids = Ci::RunnerProject + .where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})") + .select(:runner_id) Ci::Runner.specific.where(id: runner_ids) end end @@ -1033,11 +1033,14 @@ class User < ActiveRecord::Base super end - def ensure_external_user_rights - return unless external? - - self.can_create_group = false - self.projects_limit = 0 + def ensure_user_rights_and_limits + if external? + self.can_create_group = false + self.projects_limit = 0 + else + self.can_create_group = gitlab_config.default_can_create_group + self.projects_limit = current_application_settings.default_projects_limit + end end def signup_domain_valid? diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index c771c22f46a..224eb3cd4d0 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -22,16 +22,16 @@ class WikiPage def self.group_by_directory(pages) return [] if pages.blank? - pages.sort_by { |page| [page.directory, page.slug] }. - group_by(&:directory). - map do |dir, pages| + pages.sort_by { |page| [page.directory, page.slug] } + .group_by(&:directory) + .map do |dir, pages| if dir.present? WikiDirectory.new(dir, pages) else pages end - end. - flatten + end + .flatten end def self.unhyphenize(name) diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 4757ba71680..2683aaad981 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -10,7 +10,7 @@ class GlobalPolicy < BasePolicy can! :access_api can! :access_git can! :receive_notifications - can! :use_slash_commands + can! :use_quick_actions end end end diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb index 65b204d4dd2..bd5211b8e58 100644 --- a/app/serializers/issuable_entity.rb +++ b/app/serializers/issuable_entity.rb @@ -5,7 +5,6 @@ class IssuableEntity < Grape::Entity expose :description expose :lock_version expose :milestone_id - expose :position expose :state expose :title expose :updated_by_id diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 769749c9925..942145c4a8c 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -67,8 +67,8 @@ module Ci def update_merge_requests_head_pipeline return unless pipeline.latest? - MergeRequest.where(source_project: @pipeline.project, source_branch: @pipeline.ref). - update_all(head_pipeline_id: @pipeline.id) + MergeRequest.where(source_project: @pipeline.project, source_branch: @pipeline.ref) + .update_all(head_pipeline_id: @pipeline.id) end def skip_ci? diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index beb27a5a597..cf3d4aee2bc 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -3,8 +3,8 @@ module Ci def execute(project, trigger, ref, variables = nil) trigger_request = trigger.trigger_requests.create(variables: variables) - pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref). - execute(:trigger, ignore_skip_ci: true, trigger_request: trigger_request) + pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref) + .execute(:trigger, ignore_skip_ci: true, trigger_request: trigger_request) trigger_request if pipeline.persisted? end diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index d6a4280ce4c..af84d4c7427 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -54,15 +54,15 @@ module Ci def builds_for_shared_runner new_builds. # don't run projects which have not enabled shared runners and builds - joins(:project).where(projects: { shared_runners_enabled: true }). - joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id'). - where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0'). + joins(:project).where(projects: { shared_runners_enabled: true }) + .joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id') + .where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0'). # Implement fair scheduling # this returns builds that are ordered by number of running builds # we prefer projects that don't use shared runners at all - joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id"). - order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC') + joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id") + .order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC') end def builds_for_specific_runner @@ -70,8 +70,8 @@ module Ci end def running_builds_for_shared_runners - Ci::Build.running.where(runner: Ci::Runner.shared). - group(:project_id).select(:project_id, 'count(*) AS running_builds') + Ci::Build.running.where(runner: Ci::Runner.shared) + .group(:project_id).select(:project_id, 'count(*) AS running_builds') end def new_builds diff --git a/app/services/concerns/issues/resolve_discussions.rb b/app/services/concerns/issues/resolve_discussions.rb index 910a2a15e5d..7d45b4aa26a 100644 --- a/app/services/concerns/issues/resolve_discussions.rb +++ b/app/services/concerns/issues/resolve_discussions.rb @@ -10,9 +10,9 @@ module Issues def merge_request_to_resolve_discussions_of return @merge_request_to_resolve_discussions_of if defined?(@merge_request_to_resolve_discussions_of) - @merge_request_to_resolve_discussions_of = MergeRequestsFinder.new(current_user, project_id: project.id). - execute. - find_by(iid: merge_request_to_resolve_discussions_of_iid) + @merge_request_to_resolve_discussions_of = MergeRequestsFinder.new(current_user, project_id: project.id) + .execute + .find_by(iid: merge_request_to_resolve_discussions_of_iid) end def discussions_to_resolve diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb index 46823418bb0..63b85c3de7d 100644 --- a/app/services/create_deployment_service.rb +++ b/app/services/create_deployment_service.rb @@ -2,7 +2,7 @@ class CreateDeploymentService attr_reader :job delegate :expanded_environment_name, - :environment_url, + :variables, :project, to: :job @@ -14,7 +14,8 @@ class CreateDeploymentService return unless executable? ActiveRecord::Base.transaction do - environment.external_url = environment_url if environment_url + environment.external_url = expanded_environment_url if + expanded_environment_url environment.fire_state_event(action) return unless environment.save @@ -49,6 +50,17 @@ class CreateDeploymentService @environment_options ||= job.options&.dig(:environment) || {} end + def expanded_environment_url + return @expanded_environment_url if defined?(@expanded_environment_url) + + @expanded_environment_url = + ExpandVariables.expand(environment_url, variables) if environment_url + end + + def environment_url + environment_options[:url] + end + def on_stop environment_options[:on_stop] end diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index f23a9f6d57c..bcca1386bed 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -28,8 +28,8 @@ module Files end def last_commit - @last_commit ||= Gitlab::Git::Commit. - last_for_path(@start_project.repository, @start_branch, @file_path) + @last_commit ||= Gitlab::Git::Commit + .last_for_path(@start_project.repository, @start_branch, @file_path) end def validate! diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index fb1d4aed58b..20d1fb29289 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -86,8 +86,8 @@ class GitPushService < BaseService push_commits.last(PROCESS_COMMIT_LIMIT).each do |commit| if commit.matches_cross_reference_regex? - ProcessCommitWorker. - perform_async(project.id, current_user.id, commit.to_hash, default) + ProcessCommitWorker + .perform_async(project.id, current_user.id, commit.to_hash, default) end end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index cd4d180824f..8dd0846f3bc 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -142,10 +142,10 @@ class IssuableBaseService < BaseService LabelsFinder.new(current_user, project_id: @project.id).execute end - def merge_slash_commands_into_params!(issuable) + def merge_quick_actions_into_params!(issuable) description, command_params = - SlashCommands::InterpretService.new(project, current_user). - execute(params[:description], issuable) + QuickActions::InterpretService.new(project, current_user) + .execute(params[:description], issuable) # Avoid a description already set on an issuable to be overwritten by a nil params[:description] = description if params.key?(:description) @@ -162,7 +162,7 @@ class IssuableBaseService < BaseService end def create(issuable) - merge_slash_commands_into_params!(issuable) + merge_quick_actions_into_params!(issuable) filter_params(issuable) params.delete(:state_event) diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index 3cf4b82b9f2..718a7ac1f22 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -30,8 +30,8 @@ module Issues Discussions::ResolveService.new(project, current_user, merge_request: merge_request_to_resolve_discussions_of, - follow_up_issue: issue). - execute(discussions_to_resolve) + follow_up_issue: issue) + .execute(discussions_to_resolve) end private diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb index 76d0ba67b07..43b539ded53 100644 --- a/app/services/labels/promote_service.rb +++ b/app/services/labels/promote_service.rb @@ -26,29 +26,29 @@ module Labels private def label_ids_for_merge(new_label) - LabelsFinder. - new(current_user, title: new_label.title, group_id: project.group.id). - execute(skip_authorization: true). - where.not(id: new_label). - select(:id) # Can't use pluck() to avoid object-creation because of the batching + LabelsFinder + .new(current_user, title: new_label.title, group_id: project.group.id) + .execute(skip_authorization: true) + .where.not(id: new_label) + .select(:id) # Can't use pluck() to avoid object-creation because of the batching end def update_issuables(new_label, label_ids) - LabelLink. - where(label: label_ids). - update_all(label_id: new_label) + LabelLink + .where(label: label_ids) + .update_all(label_id: new_label) end def update_issue_board_lists(new_label, label_ids) - List. - where(label: label_ids). - update_all(label_id: new_label) + List + .where(label: label_ids) + .update_all(label_id: new_label) end def update_priorities(new_label, label_ids) - LabelPriority. - where(label: label_ids). - update_all(label_id: new_label) + LabelPriority + .where(label: label_ids) + .update_all(label_id: new_label) end def update_project_labels(label_ids) diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb index 514679ed29d..d2ece354efc 100644 --- a/app/services/labels/transfer_service.rb +++ b/app/services/labels/transfer_service.rb @@ -41,16 +41,16 @@ module Labels end def group_labels_applied_to_issues - Label.joins(:issues). - where( + Label.joins(:issues) + .where( issues: { project_id: project.id }, labels: { type: 'GroupLabel', group_id: old_group.id } ) end def group_labels_applied_to_merge_requests - Label.joins(:merge_requests). - where( + Label.joins(:merge_requests) + .where( merge_requests: { target_project_id: project.id }, labels: { type: 'GroupLabel', group_id: old_group.id } ) @@ -64,15 +64,15 @@ module Labels end def update_label_links(labels, old_label_id:, new_label_id:) - LabelLink.joins(:label). - merge(labels). - where(label_id: old_label_id). - update_all(label_id: new_label_id) + LabelLink.joins(:label) + .merge(labels) + .where(label_id: old_label_id) + .update_all(label_id: new_label_id) end def update_label_priorities(old_label_id:, new_label_id:) - LabelPriority.where(project_id: project.id, label_id: old_label_id). - update_all(label_id: new_label_id) + LabelPriority.where(project_id: project.id, label_id: old_label_id) + .update_all(label_id: new_label_id) end end end diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb index f846d72498f..de3a252d6c6 100644 --- a/app/services/members/authorized_destroy_service.rb +++ b/app/services/members/authorized_destroy_service.rb @@ -26,30 +26,30 @@ module Members def unassign_issues_and_merge_requests(member) if member.is_a?(GroupMember) - issues = Issue.unscoped.select(1). - joins(:project). - where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id) + issues = Issue.unscoped.select(1) + .joins(:project) + .where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id) # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...) - IssueAssignee.unscoped. - where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues). - delete_all + IssueAssignee.unscoped + .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues) + .delete_all - MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id). - execute. - update_all(assignee_id: nil) + MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id) + .execute + .update_all(assignee_id: nil) else project = member.source # SELECT 1 FROM issues WHERE issues.id = issue_assignees.issue_id AND issues.project_id = X - issues = Issue.unscoped.select(1). - where('issues.id = issue_assignees.issue_id'). - where(project_id: project.id) + issues = Issue.unscoped.select(1) + .where('issues.id = issue_assignees.issue_id') + .where(project_id: project.id) # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...) - IssueAssignee.unscoped. - where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues). - delete_all + IssueAssignee.unscoped + .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues) + .delete_all project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil) end diff --git a/app/services/merge_requests/conflicts/resolve_service.rb b/app/services/merge_requests/conflicts/resolve_service.rb index c2c335b8461..6b6e231f4f9 100644 --- a/app/services/merge_requests/conflicts/resolve_service.rb +++ b/app/services/merge_requests/conflicts/resolve_service.rb @@ -27,10 +27,10 @@ module MergeRequests tree: merge_index.write_tree(rugged) } - conflicts_for_resolution. - project. - repository. - resolve_conflicts(current_user, merge_request.source_branch, commit_params) + conflicts_for_resolution + .project + .repository + .resolve_conflicts(current_user, merge_request.source_branch, commit_params) end end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index fac3ac7a4c7..b247cb89e5e 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -61,8 +61,8 @@ module MergeRequests MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) if params[:should_remove_source_branch].present? || @merge_request.force_remove_source_branch? - DeleteBranchService.new(@merge_request.source_project, branch_deletion_user). - execute(merge_request.source_branch) + DeleteBranchService.new(@merge_request.source_project, branch_deletion_user) + .execute(merge_request.source_branch) end end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 81d217929d5..e0e7c43f802 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -43,9 +43,9 @@ module MergeRequests end filter_merge_requests(merge_requests).each do |merge_request| - MergeRequests::PostMergeService. - new(merge_request.target_project, @current_user). - execute(merge_request) + MergeRequests::PostMergeService + .new(merge_request.target_project, @current_user) + .execute(merge_request) end end @@ -56,8 +56,8 @@ module MergeRequests # Refresh merge request diff if we push to source or target branch of merge request # Note: we should update merge requests from forks too def reload_merge_requests - merge_requests = @project.merge_requests.opened. - by_source_or_target_branch(@branch_name).to_a + merge_requests = @project.merge_requests.opened + .by_source_or_target_branch(@branch_name).to_a # Fork merge requests merge_requests += MergeRequest.opened diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 5c843a258fb..75a65aecd1a 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -7,7 +7,7 @@ module MergeRequests params.except!(:target_project_id) params.except!(:source_branch) - merge_from_slash_command(merge_request) if params[:merge] + merge_from_quick_action(merge_request) if params[:merge] if merge_request.closed_without_fork? params.except!(:target_branch, :force_remove_source_branch) @@ -74,9 +74,9 @@ module MergeRequests end end - def merge_from_slash_command(merge_request) + def merge_from_quick_action(merge_request) last_diff_sha = params.delete(:merge) - return unless merge_request.mergeable_with_slash_command?(current_user, last_diff_sha: last_diff_sha) + return unless merge_request.mergeable_with_quick_action?(current_user, last_diff_sha: last_diff_sha) merge_request.update(merge_error: nil) diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index f3954f6f8c4..06971483992 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -9,11 +9,11 @@ module Notes # We execute commands (extracted from `params[:note]`) on the noteable # **before** we save the note because if the note consists of commands # only, there is no need be create a note! - slash_commands_service = SlashCommandsService.new(project, current_user) + quick_actions_service = QuickActionsService.new(project, current_user) - if slash_commands_service.supported?(note) + if quick_actions_service.supported?(note) options = { merge_request_diff_head_sha: merge_request_diff_head_sha } - content, command_params = slash_commands_service.extract_commands(note, options) + content, command_params = quick_actions_service.extract_commands(note, options) only_commands = content.empty? @@ -30,7 +30,7 @@ module Notes end if command_params.present? - slash_commands_service.execute(command_params, note) + quick_actions_service.execute(command_params, note) # We must add the error after we call #save because errors are reset # when #save is called diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/quick_actions_service.rb index ad1e6f6774a..a8d0cc15527 100644 --- a/app/services/notes/slash_commands_service.rb +++ b/app/services/notes/quick_actions_service.rb @@ -1,5 +1,5 @@ module Notes - class SlashCommandsService < BaseService + class QuickActionsService < BaseService UPDATE_SERVICES = { 'Issue' => Issues::UpdateService, 'MergeRequest' => MergeRequests::UpdateService @@ -22,8 +22,8 @@ module Notes def extract_commands(note, options = {}) return [note.note, {}] unless supported?(note) - SlashCommands::InterpretService.new(project, current_user, options). - execute(note.note, note.noteable) + QuickActions::InterpretService.new(project, current_user, options) + .execute(note.note, note.noteable) end def execute(command_params, note) diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb index 10d45bbf73c..4ee2c1796bd 100644 --- a/app/services/preview_markdown_service.rb +++ b/app/services/preview_markdown_service.rb @@ -1,6 +1,6 @@ class PreviewMarkdownService < BaseService def execute - text, commands = explain_slash_commands(params[:text]) + text, commands = explain_quick_actions(params[:text]) users = find_user_references(text) success( @@ -12,11 +12,11 @@ class PreviewMarkdownService < BaseService private - def explain_slash_commands(text) + def explain_quick_actions(text) return text, [] unless %w(Issue MergeRequest).include?(commands_target_type) - slash_commands_service = SlashCommands::InterpretService.new(project, current_user) - slash_commands_service.explain(text, find_commands_target) + quick_actions_service = QuickActions::InterpretService.new(project, current_user) + quick_actions_service.explain(text, find_commands_target) end def find_user_references(text) @@ -36,10 +36,10 @@ class PreviewMarkdownService < BaseService end def commands_target_type - params[:slash_commands_target_type] + params[:quick_actions_target_type] end def commands_target_id - params[:slash_commands_target_id] + params[:quick_actions_target_id] end end diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index 015f2828921..fc85f398935 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -32,7 +32,7 @@ module Projects issuable: noteable, current_user: current_user } - SlashCommands::InterpretService.command_definitions.map do |definition| + QuickActions::InterpretService.command_definitions.map do |definition| next unless definition.available?(opts) definition.to_h(opts) diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 1c24b27a870..fd701e33524 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -12,87 +12,121 @@ module Projects TransferError = Class.new(StandardError) def execute(new_namespace) - if new_namespace.blank? + @new_namespace = new_namespace + + if @new_namespace.blank? raise TransferError, 'Please select a new namespace for your project.' end - unless allowed_transfer?(current_user, project, new_namespace) + + unless allowed_transfer?(current_user, project) raise TransferError, 'Transfer failed, please contact an admin.' end - transfer(project, new_namespace) + + transfer(project) + + true rescue Projects::TransferService::TransferError => ex project.reload project.errors.add(:new_namespace, ex.message) false end - def transfer(project, new_namespace) - old_namespace = project.namespace + private - Project.transaction do - old_path = project.path_with_namespace - old_group = project.group - new_path = File.join(new_namespace.try(:full_path) || '', project.path) + def transfer(project) + @old_path = project.path_with_namespace + @old_group = project.group + @new_path = File.join(@new_namespace.try(:full_path) || '', project.path) + @old_namespace = project.namespace - if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present? - raise TransferError.new("Project with same path in target namespace already exists") - end + if Project.where(path: project.path, namespace_id: @new_namespace.try(:id)).exists? + raise TransferError.new("Project with same path in target namespace already exists") + end - if project.has_container_registry_tags? - # we currently doesn't support renaming repository if it contains tags in container registry - raise TransferError.new('Project cannot be transferred, because tags are present in its container registry') - end + if project.has_container_registry_tags? + # We currently don't support renaming repository if it contains tags in container registry + raise TransferError.new('Project cannot be transferred, because tags are present in its container registry') + end - project.expire_caches_before_rename(old_path) + attempt_transfer_transaction + end + + def attempt_transfer_transaction + Project.transaction do + project.expire_caches_before_rename(@old_path) - # Apply new namespace id and visibility level - project.namespace = new_namespace - project.visibility_level = new_namespace.visibility_level unless project.visibility_level_allowed_by_group? - project.save! + update_namespace_and_visibility(@new_namespace) # Notifications - project.send_move_instructions(old_path) + project.send_move_instructions(@old_path) # Move main repository - unless gitlab_shell.mv_repository(project.repository_storage_path, old_path, new_path) + unless move_repo_folder(@old_path, @new_path) raise TransferError.new('Cannot move project') end # Move wiki repo also if present - gitlab_shell.mv_repository(project.repository_storage_path, "#{old_path}.wiki", "#{new_path}.wiki") + move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki") # Move missing group labels to project - Labels::TransferService.new(current_user, old_group, project).execute + Labels::TransferService.new(current_user, @old_group, project).execute # Move uploads - Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.full_path, new_namespace.full_path) + Gitlab::UploadsTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path) # Move pages - Gitlab::PagesTransfer.new.move_project(project.path, old_namespace.full_path, new_namespace.full_path) + Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path) - project.old_path_with_namespace = old_path + project.old_path_with_namespace = @old_path - SystemHooksService.new.execute_hooks_for(project, :transfer) + execute_system_hooks end - - refresh_permissions(old_namespace, new_namespace) - - true + rescue Exception # rubocop:disable Lint/RescueException + rollback_side_effects + raise + ensure + refresh_permissions end - def allowed_transfer?(current_user, project, namespace) - namespace && + def allowed_transfer?(current_user, project) + @new_namespace && can?(current_user, :change_namespace, project) && - namespace.id != project.namespace_id && - current_user.can?(:create_projects, namespace) + @new_namespace.id != project.namespace_id && + current_user.can?(:create_projects, @new_namespace) end - def refresh_permissions(old_namespace, new_namespace) + def update_namespace_and_visibility(to_namespace) + # Apply new namespace id and visibility level + project.namespace = to_namespace + project.visibility_level = to_namespace.visibility_level unless project.visibility_level_allowed_by_group? + project.save! + end + + def refresh_permissions # This ensures we only schedule 1 job for every user that has access to # the namespaces. - user_ids = old_namespace.user_ids_for_project_authorizations | - new_namespace.user_ids_for_project_authorizations + user_ids = @old_namespace.user_ids_for_project_authorizations | + @new_namespace.user_ids_for_project_authorizations UserProjectAccessChangedService.new(user_ids).execute end + + def rollback_side_effects + rollback_folder_move + update_namespace_and_visibility(@old_namespace) + end + + def rollback_folder_move + move_repo_folder(@new_path, @old_path) + move_repo_folder("#{@new_path}.wiki", "#{@old_path}.wiki") + end + + def move_repo_folder(from_name, to_name) + gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name) + end + + def execute_system_hooks + SystemHooksService.new.execute_hooks_for(project, :transfer) + end end end diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 83144b1e011..6816b137361 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -1,13 +1,13 @@ -module SlashCommands +module QuickActions class InterpretService < BaseService - include Gitlab::SlashCommands::Dsl + include Gitlab::QuickActions::Dsl attr_reader :issuable # Takes a text and interprets the commands that are extracted from it. # Returns the content without commands, and hash of changes to be applied to a record. def execute(content, issuable) - return [content, {}] unless current_user.can?(:use_slash_commands) + return [content, {}] unless current_user.can?(:use_quick_actions) @issuable = issuable @updates = {} @@ -20,7 +20,7 @@ module SlashCommands # Takes a text and interprets the commands that are extracted from it. # Returns the content without commands, and array of changes explained. def explain(content, issuable) - return [content, []] unless current_user.can?(:use_slash_commands) + return [content, []] unless current_user.can?(:use_quick_actions) @issuable = issuable @@ -32,7 +32,7 @@ module SlashCommands private def extractor - Gitlab::SlashCommands::Extractor.new(self.class.command_definitions) + Gitlab::QuickActions::Extractor.new(self.class.command_definitions) end desc do @@ -71,7 +71,7 @@ module SlashCommands last_diff_sha = params && params[:merge_request_diff_head_sha] issuable.is_a?(MergeRequest) && issuable.persisted? && - issuable.mergeable_with_slash_command?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha) + issuable.mergeable_with_quick_action?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha) end command :merge do @updates[:merge] = params[:merge_request_diff_head_sha] diff --git a/app/services/tags/create_service.rb b/app/services/tags/create_service.rb index 1756da9e519..674792f6138 100644 --- a/app/services/tags/create_service.rb +++ b/app/services/tags/create_service.rb @@ -19,8 +19,8 @@ module Tags if new_tag if release_description - CreateReleaseService.new(@project, @current_user). - execute(tag_name, release_description) + CreateReleaseService.new(@project, @current_user) + .execute(tag_name, release_description) end success.merge(tag: new_tag) diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb index 3e07b811027..f028f5eb0d4 100644 --- a/app/services/users/refresh_authorized_projects_service.rb +++ b/app/services/users/refresh_authorized_projects_service.rb @@ -34,7 +34,7 @@ module Users # Keep trying until we obtain the lease. If we don't do so we may end up # not updating the list of authorized projects properly. To prevent # hammering Redis too much we'll wait for a bit between retries. - sleep(1) + sleep(0.1) end begin diff --git a/app/validators/dynamic_path_validator.rb b/app/validators/dynamic_path_validator.rb index 27ac60637fd..4688aabc2a8 100644 --- a/app/validators/dynamic_path_validator.rb +++ b/app/validators/dynamic_path_validator.rb @@ -26,7 +26,7 @@ class DynamicPathValidator < ActiveModel::EachValidator end def path_valid_for_record?(record, value) - full_path = record.respond_to?(:full_path) ? record.full_path : value + full_path = record.respond_to?(:build_full_path) ? record.build_full_path : value return true unless full_path diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 95dffdafabe..b21d5665970 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -325,6 +325,10 @@ = f.label :prometheus_metrics_enabled do = f.check_box :prometheus_metrics_enabled Enable Prometheus Metrics + - unless Gitlab::Metrics.metrics_folder_present? + .help-block + %strong.cred WARNING: + Environment variable `prometheus_multiproc_dir` does not exist or is not pointing to a valid directory. %fieldset %legend Background Jobs diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml index 2269fb1fd8c..5a4ed1c3a2a 100644 --- a/app/views/admin/broadcast_messages/_form.html.haml +++ b/app/views/admin/broadcast_messages/_form.html.haml @@ -21,11 +21,11 @@ .form-group.js-toggle-colors-container.hide = f.label :color, "Background Color", class: 'control-label' .col-sm-10 - = f.text_field :color, class: "form-control" + = f.color_field :color, class: "form-control" .form-group.js-toggle-colors-container.hide = f.label :font, "Font Color", class: 'control-label' .col-sm-10 - = f.text_field :font, class: "form-control" + = f.color_field :font, class: "form-control" .form-group = f.label :starts_at, class: 'control-label' .col-sm-10.datetime-controls diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml index a83faa839df..b7a60938132 100644 --- a/app/views/notify/pipeline_failed_email.html.haml +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -60,7 +60,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.author %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } @@ -76,7 +76,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.committer %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" } @@ -100,7 +100,7 @@ triggered by - if @pipeline.user %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" } - %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" } = @pipeline.user.name diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml index 9c2e2a599b2..3f16885b8e3 100644 --- a/app/views/notify/pipeline_success_email.html.haml +++ b/app/views/notify/pipeline_success_email.html.haml @@ -60,7 +60,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.author %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } @@ -76,7 +76,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.committer %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" } @@ -100,7 +100,7 @@ triggered by - if @pipeline.user %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" } - %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" } = @pipeline.user.name diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index fcfd350f0da..15672289c65 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -42,10 +42,17 @@ - if current_user.ldap_user? Some options are unavailable for LDAP accounts .col-lg-9 - .form-group - = f.label :name, class: "label-light" - = f.text_field :name, class: "form-control", required: true - %span.help-block Enter your name, so people you know can recognize you. + .row + .form-group.col-md-9 + = f.label :name, class: "label-light" + = f.text_field :name, class: "form-control", required: true + %span.help-block Enter your name, so people you know can recognize you. + + .form-group.col-md-3 + = f.label :id, class: 'label-light' do + User ID + = f.text_field :id, class: 'form-control', readonly: true + .form-group = f.label :email, class: "label-light" diff --git a/app/views/projects/_find_file_link.html.haml b/app/views/projects/_find_file_link.html.haml index c748ccf65e6..cb4d2bbacf5 100644 --- a/app/views/projects/_find_file_link.html.haml +++ b/app/views/projects/_find_file_link.html.haml @@ -1,3 +1,3 @@ -= link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn btn-grouped shortcuts-find-file', rel: 'nofollow' do += link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn shortcuts-find-file', rel: 'nofollow' do = icon('search') %span= _('Find file') diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 07445434cf3..d0698285f84 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -9,6 +9,12 @@ %li %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } Preview + + - if defined?(@issue) && @issue.confidential? + %li.confidential-issue-warning + = icon('warning') + %span This is a confidential issue. Your comment will not be visible to the public. + %li.pull-right .toolbar-group = markdown_toolbar_button({ icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" }) diff --git a/app/views/projects/_visibility_select.html.haml b/app/views/projects/_visibility_select.html.haml index 65fc0a36ca9..4026b9e3c46 100644 --- a/app/views/projects/_visibility_select.html.haml +++ b/app/views/projects/_visibility_select.html.haml @@ -1,5 +1,7 @@ - if can_change_visibility_level?(@project, current_user) - = form.select(model_method, visibility_select_options(@project, selected_level), {}, class: 'form-control visibility-select') + .select-wrapper + = form.select(model_method, visibility_select_options(@project, selected_level), {}, class: 'form-control visibility-select select-control') + = icon('chevron-down') - else .info.js-locked{ data: { help_block: visibility_level_description(@project.visibility_level, @project) } } = visibility_level_icon(@project.visibility_level) diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index 3b3d08ddd3c..afc40ca4eab 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -1,10 +1,15 @@ - @gfm_form = true - current_text ||= nil -- supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false) +- supports_autocomplete = local_assigns.fetch(:supports_autocomplete, true) +- supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false) .zen-backdrop - classes << ' js-gfm-input js-autosize markdown-area' - if defined?(f) && f - = f.text_area attr, class: classes, placeholder: placeholder, data: { supports_slash_commands: supports_slash_commands } + = f.text_area attr, + class: classes, + placeholder: placeholder, + data: { supports_quick_actions: supports_quick_actions, + supports_autocomplete: supports_autocomplete } - else = text_area_tag attr, current_text, class: classes, placeholder: placeholder %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" } diff --git a/app/views/projects/blob/_breadcrumb.html.haml b/app/views/projects/blob/_breadcrumb.html.haml index 0ad9f258e48..5840e9863f4 100644 --- a/app/views/projects/blob/_breadcrumb.html.haml +++ b/app/views/projects/blob/_breadcrumb.html.haml @@ -1,9 +1,26 @@ - blame = local_assigns.fetch(:blame, false) .nav-block + .tree-ref-container + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'blob', path: @path + + %ul.breadcrumb.repo-breadcrumb + %li + = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do + = @project.path + - path_breadcrumbs do |title, path| + - title = truncate(title, length: 40) + %li + - if path == @path + = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, path)) do + %strong= title + - else + = link_to title, namespace_project_tree_path(@project.namespace, @project, tree_join(@ref, path)) + .tree-controls = render 'projects/find_file_link' - .btn-group.prepend-left-10{ role: "group" }< + .btn-group{ role: "group" }< -# only show normal/blame view links for text files - if blob.readable_text? - if blame @@ -18,19 +35,3 @@ = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.sha, @path)), class: 'btn js-data-file-blob-permalink-url' - - .tree-ref-holder - = render 'shared/ref_switcher', destination: 'blob', path: @path - - %ul.breadcrumb.repo-breadcrumb - %li - = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do - = @project.path - - path_breadcrumbs do |title, path| - - title = truncate(title, length: 40) - %li - - if path == @path - = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, path)) do - %strong= title - - else - = link_to title, namespace_project_tree_path(@project.namespace, @project, tree_join(@ref, path)) diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index e14885f264b..32dbc1b3417 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -9,8 +9,10 @@ .dropzone .dropzone-previews.blob-upload-dropzone-previews %p.dz-message.light - Attach a file by drag & drop or - = link_to 'click to upload', '#', class: "markdown-selector" + - upload_link = link_to s_('UploadLink|click to upload'), '#', class: "markdown-selector" + - dropzone_text = _('Attach a file by drag & drop or %{upload_link}') % { upload_link: upload_link } + #{ dropzone_text.html_safe } + %br .dropzone-alerts.alert.alert-danger.data{ style: "display:none" } @@ -18,7 +20,7 @@ .form-actions = button_tag button_title, class: 'btn btn-small btn-create btn-upload-file', id: 'submit-all' - = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" + = link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal" - unless can?(current_user, :push_code, @project) .inline.prepend-left-10 diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 3cf91bf07f7..a73ddd5eb33 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -2,7 +2,7 @@ - if !project.empty_repo? && can?(current_user, :download_code, project) .project-action-button.dropdown.inline> - %button.btn.has-tooltip{ title: 'Download', 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download') } + %button.btn.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download') } = icon('download') = icon("caret-down") %span.sr-only= _('Select Archive Format') diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 312c349da3a..960b57a8008 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -1,6 +1,6 @@ - if current_user .project-action-button.dropdown.inline - %a.btn.dropdown-toggle.has-tooltip{ href: '#', title: 'Create new...', 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => 'Create new...' } + %a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...') } = icon('plus') = icon("caret-down") %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 7a08bb37494..42f8c75f57b 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -4,11 +4,15 @@ = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: _('Go to your fork'), class: 'btn has-tooltip' do = custom_icon('icon_fork') %span= s_('GoToYourFork|Fork') + - elsif !current_user.can_create_project? + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: _('You have reached your project limit'), class: 'btn has-tooltip disabled' do + = custom_icon('icon_fork') + %span= s_('CreateNewFork|Fork') - else = link_to new_namespace_project_fork_path(@project.namespace, @project), class: 'btn' do = custom_icon('icon_fork') %span= s_('CreateNewFork|Fork') .count-with-arrow %span.arrow - = link_to namespace_project_forks_path(@project.namespace, @project), title: n_('Forks', @project.forks_count), class: 'count' do + = link_to namespace_project_forks_path(@project.namespace, @project), title: n_('Fork', 'Forks', @project.forks_count), class: 'count' do = @project.forks_count diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index 281d823da52..2267f123e38 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -1,35 +1,36 @@ - case type.to_s - when 'revert' - - label = 'Revert' - - branch_label = 'Revert in branch' + - label = s_('ChangeTypeAction|Revert') + - branch_label = s_('ChangeTypeActionLabel|Revert in branch') + - revert_merge_request = _('Revert this merge request') + - revert_commit = _('Revert this commit') + - title = commit.merged_merge_request(current_user) ? revert_merge_request : revert_commit - when 'cherry-pick' - - label = 'Cherry-pick' - - branch_label = 'Pick into branch' + - label = s_('ChangeTypeAction|Cherry-pick') + - branch_label = s_('ChangeTypeActionLabel|Pick into branch') + - title = commit.merged_merge_request(current_user) ? _('Cherry-pick this merge request') : _('Cherry-pick this commit') .modal{ id: "modal-#{type}-commit" } .modal-dialog .modal-content .modal-header %a.close{ href: "#", "data-dismiss" => "modal" } × - %h3.page-title== #{label} this #{commit.change_type_title(current_user)} + %h3.page-title= title .modal-body = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do .form-group.branch = label_tag 'start_branch', branch_label, class: 'control-label' .col-sm-10 = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch' - = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } }) + = dropdown_tag(@project.default_branch, options: { title: s_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: s_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } }) - if can?(current_user, :push_code, @project) - .checkbox - = label_tag do - = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: nil - Start a <strong>new merge request</strong> with these changes + = render 'shared/new_merge_request_checkbox' - else = hidden_field_tag 'create_merge_request', 1, id: nil .form-actions = submit_tag label, class: 'btn btn-create' - = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" + = link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal" - unless can?(current_user, :push_code, @project) .inline.prepend-left-10 diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index aab50310234..7fe44816bae 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,17 +1,17 @@ .page-content-header .header-main-content %strong - Commit + #{ s_('CommitBoxTitle|Commit') } %span.commit-sha= @commit.short_id - = clipboard_button(text: @commit.id, title: "Copy commit SHA to clipboard") + = clipboard_button(text: @commit.id, title: _("Copy commit SHA to clipboard")) %span.hidden-xs authored #{time_ago_with_tooltip(@commit.authored_date)} - %span by + %span= s_('ByAuthor|by') = author_avatar(@commit, size: 24) %strong = commit_author_link(@commit, avatar: true, size: 24) - if @commit.different_committer? - %span.light Committed by + %span.light= _('Committed by') %strong = commit_committer_link(@commit, avatar: true, size: 24) #{time_ago_with_tooltip(@commit.committed_date)} @@ -22,15 +22,15 @@ = icon('comment') = @notes_count = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-default append-right-10 hidden-xs hidden-sm" do - Browse files + #{ _('Browse files') } .dropdown.inline %a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } } - %span Options + %span= _('Options') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right %li.visible-xs-block.visible-sm-block = link_to namespace_project_tree_path(@project.namespace, @project, @commit) do - Browse Files + _('Browse Files') - unless @commit.has_been_reverted?(current_user) %li.clearfix = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) @@ -38,13 +38,13 @@ = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) - if can_collaborate_with_project? %li.clearfix - = link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit) + = link_to s_("CreateTag|Tag"), new_namespace_project_tag_path(@project.namespace, @project, ref: @commit) %li.divider %li.dropdown-header - Download + #{ _('Download') } - unless @commit.parents.length > 1 - %li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch) - %li= link_to "Plain Diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff) + %li= link_to s_("DownloadCommit|Email Patches"), namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch) + %li= link_to s_("DownloadCommit|Plain Diff"), namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff) .commit-box %h3.commit-title @@ -57,7 +57,7 @@ .well-segment.branch-info .icon-container.commit-icon = custom_icon("icon_commit") - %span.cgray= pluralize(@commit.parents.count, "parent") + %span.cgray= n_('parent', 'parents', @commit.parents.count) - @commit.parents.each do |parent| = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "commit-sha" %span.commit-info.branches @@ -69,11 +69,11 @@ .status-icon-container{ class: "ci-status-icon-#{@commit.status}" } = link_to namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id) do = ci_icon_for_status(last_pipeline.status) - Pipeline + #{ _('Pipeline') } = link_to "##{last_pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id) = ci_label_for_status(last_pipeline.status) - if last_pipeline.stages_count.nonzero? - with #{"stage".pluralize(last_pipeline.stages_count)} + #{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), last_pipeline.stages_count) } .mr-widget-pipeline-graph = render 'shared/mini_pipeline_graph', pipeline: last_pipeline, klass: 'js-commit-pipeline-graph' in diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 7a03c3561af..11de6915961 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -30,9 +30,11 @@ %pre.commit-row-description.js-toggle-content = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author)) .commiter - = commit_author_link(commit, avatar: false, size: 24) - #{ _('committed') } - #{time_ago_with_tooltip(commit.committed_date)} + - commit_author_link = commit_author_link(commit, avatar: false, size: 24) + - commit_timeago = time_ago_with_tooltip(commit.committed_date) + - commit_text = _('%{commit_author_link} committed %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago } + #{ commit_text.html_safe } + .commit-actions.flex-row.hidden-xs - if commit.status(ref) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index c3dab68cea5..296e37e20e6 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -99,9 +99,9 @@ Git Large File Storage = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') .col-md-3 - = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select', data: { field: 'lfs_enabled' } - - + .select-wrapper + = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' } + = icon('chevron-down') - if Gitlab.config.registry.enabled .form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) } .checkbox diff --git a/app/views/projects/issues/_issue_by_email.html.haml b/app/views/projects/issues/_issue_by_email.html.haml index da65157a10b..35b7d1b920c 100644 --- a/app/views/projects/issues/_issue_by_email.html.haml +++ b/app/views/projects/issues/_issue_by_email.html.haml @@ -20,7 +20,7 @@ %p The subject will be used as the title of the new issue, and the message will be the description. - = link_to 'Slash commands', help_page_path('user/project/slash_commands'), target: '_blank', tabindex: -1 + = link_to 'Quick actions', help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1 and styling with = link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1 are supported. diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 5f92d020eef..d909b0bfbbd 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -5,13 +5,6 @@ - can_update_issue = can?(current_user, :update_issue, @issue) - can_report_spam = @issue.submittable_as_spam_by?(current_user) -- if defined?(@issue) && @issue.confidential? - .confidential-issue-warning{ data: { spy: 'affix' } } - %span.confidential-issue-text - #{confidential_icon(@issue)} This issue is confidential. - %a{ href: help_page_path('user/project/issues/confidential_issues'), target: '_blank' } - What are confidential issues? - .clearfix.detail-page-header .issuable-header .issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) } @@ -26,6 +19,7 @@ = icon('angle-double-left') .issuable-meta + = confidential_icon(@issue) = issuable_meta(@issue, @project, "Issue") .issuable-actions diff --git a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml index 62c9748c510..e675e1830d0 100644 --- a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml +++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml @@ -1,7 +1,7 @@ .form-horizontal.resolve-conflicts-form .form-group %label.col-sm-2.control-label{ "for" => "commit-message" } - Commit message + #{ _('Commit message') } .col-sm-10 .commit-message-container .max-width-marker diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml index e0d45054854..75a4687e1e3 100644 --- a/app/views/projects/notes/_more_actions_dropdown.html.haml +++ b/app/views/projects/notes/_more_actions_dropdown.html.haml @@ -1,14 +1,19 @@ -.dropdown.more-actions - = button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body' } do - = icon('ellipsis-v', class: 'icon') - %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left - %li - = button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent' - %li.divider - %li - = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do - Report as abuse - - if note_editable - %li - = link_to note_url(note), method: :delete, data: { confirm: 'Are you sure you want to delete this comment?' }, remote: true, class: 'js-note-delete' do - %span.text-danger Delete comment +- is_current_user = current_user == note.author + +- if note_editable || !is_current_user + .dropdown.more-actions + = button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body' } do + = icon('ellipsis-v', class: 'icon') + %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left + - if note_editable + %li + = button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent' + %li.divider + - unless is_current_user + %li + = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do + Report as abuse + - if note_editable + %li + = link_to note_url(note), method: :delete, data: { confirm: 'Are you sure you want to delete this comment?' }, remote: true, class: 'js-note-delete' do + %span.text-danger Delete comment diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml index e8dedf26206..fc7fa5c1876 100644 --- a/app/views/projects/pipeline_schedules/_form.html.haml +++ b/app/views/projects/pipeline_schedules/_form.html.haml @@ -7,7 +7,7 @@ .form-group .col-md-9 = f.label :description, _('Description'), class: 'label-light' - = f.text_field :description, class: 'form-control', required: true, autofocus: true, placeholder: _('PipelineSchedules|Provide a short description for this pipeline') + = f.text_field :description, class: 'form-control', required: true, autofocus: true, placeholder: s_('PipelineSchedules|Provide a short description for this pipeline') .form-group .col-md-9 = f.label :cron, _('Interval Pattern'), class: 'label-light' @@ -15,19 +15,19 @@ .form-group .col-md-9 = f.label :cron_timezone, _('Cron Timezone'), class: 'label-light' - = dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown', title: _("Select a timezone"), filter: true, placeholder: _("Filter"), data: { data: timezone_data } } ) + = dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown', title: _("Select a timezone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } ) = f.text_field :cron_timezone, value: @schedule.cron_timezone, id: 'schedule_cron_timezone', class: 'hidden', name: 'schedule[cron_timezone]', required: true .form-group .col-md-9 = f.label :ref, _('Target Branch'), class: 'label-light' - = dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: _("Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } ) + = dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } ) = f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true .form-group .col-md-9 - = f.label :active, _('PipelineSchedules|Activated'), class: 'label-light' + = f.label :active, s_('PipelineSchedules|Activated'), class: 'label-light' %div = f.check_box :active, required: false, value: @schedule.active? - Active + = _('Active') .footer-block.row-content-block = f.submit _('Save pipeline schedule'), class: 'btn btn-create', tabindex: 3 = link_to _('Cancel'), pipeline_schedules_path(@project), class: 'btn btn-cancel' diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml index 2d3344a4aaf..966d6cd8495 100644 --- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml +++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml @@ -13,12 +13,12 @@ = ci_icon_for_status(pipeline_schedule.last_pipeline.status) %span ##{pipeline_schedule.last_pipeline.id} - else - = _("PipelineSchedules|None") + = s_("PipelineSchedules|None") %td.next-run-cell - if pipeline_schedule.active? = time_ago_with_tooltip(pipeline_schedule.real_next_run) - else - = _("PipelineSchedules|Inactive") + = s_("PipelineSchedules|Inactive") %td - if pipeline_schedule.owner = image_tag avatar_icon(pipeline_schedule.owner, 20), class: "avatar s20" diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml index 4a96ee652d2..c296152e54f 100644 --- a/app/views/projects/pipeline_schedules/index.html.haml +++ b/app/views/projects/pipeline_schedules/index.html.haml @@ -14,7 +14,7 @@ .nav-controls = link_to new_namespace_project_pipeline_schedule_path(@project.namespace, @project), class: 'btn btn-create' do - %span New schedule + %span= _('New schedule') - if @schedules.present? %ul.content-list diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index 247c4bdbe2d..8bf2246662a 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -6,7 +6,9 @@ = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true, placeholder: "Search for members to update or invite") .form-group = label_tag :access_level, "Choose a role permission", class: "label-light" - = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select" + .select-wrapper + = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select select-control" + = icon('chevron-down') .help-block.append-bottom-10 = link_to "Read more", help_page_path("user/permissions"), class: "vlink" about role permissions diff --git a/app/views/projects/project_members/_new_shared_group.html.haml b/app/views/projects/project_members/_new_shared_group.html.haml index b7cc8dd7062..643569db646 100644 --- a/app/views/projects/project_members/_new_shared_group.html.haml +++ b/app/views/projects/project_members/_new_shared_group.html.haml @@ -8,7 +8,7 @@ = label_tag :link_group_access, "Max access level", class: "label-light" .select-wrapper = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" - = icon('caret-down') + = icon('chevron-down') .help-block.append-bottom-10 = link_to "Read more", help_page_path("user/permissions"), class: "vlink" about role permissions diff --git a/app/views/projects/registry/repositories/_image.html.haml b/app/views/projects/registry/repositories/_image.html.haml index 8bc78f8d018..dcdc432b654 100644 --- a/app/views/projects/registry/repositories/_image.html.haml +++ b/app/views/projects/registry/repositories/_image.html.haml @@ -6,13 +6,14 @@ = clipboard_button(clipboard_text: "docker pull #{image.location}") - .controls.hidden-xs.pull-right - = link_to namespace_project_container_registry_path(@project.namespace, @project, image), - class: 'btn btn-remove has-tooltip', - title: 'Remove repository', - data: { confirm: 'Are you sure?' }, - method: :delete do - = icon('trash cred', 'aria-hidden': 'true') + - if can?(current_user, :update_container_image, @project) + .controls.hidden-xs.pull-right + = link_to namespace_project_container_registry_path(@project.namespace, @project, image), + class: 'btn btn-remove has-tooltip', + title: 'Remove repository', + data: { confirm: 'Are you sure?' }, + method: :delete do + = icon('trash cred', 'aria-hidden': 'true') .container-image-tags.js-toggle-content.hide - if image.has_tags? diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index abde2a48587..00da76349da 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -1,79 +1,81 @@ -.tree-controls - = render 'projects/find_file_link' - - = link_to s_('Commits|History'), namespace_project_commits_path(@project.namespace, @project, @id), class: 'btn btn-grouped' - - = render 'projects/buttons/download', project: @project, ref: @ref +.tree-ref-container + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'tree', path: @path -.tree-ref-holder - = render 'shared/ref_switcher', destination: 'tree', path: @path - -%ul.breadcrumb.repo-breadcrumb - %li - = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do - = @project.path - - path_breadcrumbs do |title, path| + %ul.breadcrumb.repo-breadcrumb %li - = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, tree_join(@ref, path)) + = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do + = @project.path + - path_breadcrumbs do |title, path| + %li + = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, tree_join(@ref, path)) - - if current_user - %li - - if !on_top_of_branch? - %span.btn.add-to-tree.disabled.has-tooltip{ title: _("You can only add files when you are on a branch"), data: { container: 'body' } } - = icon('plus') - - else - %span.dropdown - %a.dropdown-toggle.btn.add-to-tree{ href: '#', "data-toggle" => "dropdown" } + - if current_user + %li + - if !on_top_of_branch? + %span.btn.add-to-tree.disabled.has-tooltip{ title: _("You can only add files when you are on a branch"), data: { container: 'body' } } = icon('plus') - %ul.dropdown-menu - - if can_edit_tree? - %li - = link_to namespace_project_new_blob_path(@project.namespace, @project, @id) do - = icon('pencil fw') - #{ _('New file') } - %li - = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do - = icon('file fw') - #{ _('Upload file') } - %li - = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do - = icon('folder fw') - #{ _('New directory') } - - elsif can?(current_user, :fork_project, @project) - %li - - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @id), - notice: edit_in_new_fork_notice, - notice_now: edit_in_new_fork_notice_now } - - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, - continue: continue_params) - = link_to fork_path, method: :post do - = icon('pencil fw') - #{ _('New file') } + - else + %span.dropdown + %a.dropdown-toggle.btn.add-to-tree{ href: '#', "data-toggle" => "dropdown", "data-target" => ".add-to-tree-dropdown" } + = icon('plus') + .add-to-tree-dropdown + %ul.dropdown-menu + - if can_edit_tree? + %li + = link_to namespace_project_new_blob_path(@project.namespace, @project, @id) do + = icon('pencil fw') + #{ _('New file') } + %li + = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do + = icon('file fw') + #{ _('Upload file') } + %li + = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do + = icon('folder fw') + #{ _('New directory') } + - elsif can?(current_user, :fork_project, @project) + %li + - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @id), + notice: edit_in_new_fork_notice, + notice_now: edit_in_new_fork_notice_now } + - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + continue: continue_params) + = link_to fork_path, method: :post do + = icon('pencil fw') + #{ _('New file') } + %li + - continue_params = { to: request.fullpath, + notice: edit_in_new_fork_notice + " Try to upload a file again.", + notice_now: edit_in_new_fork_notice_now } + - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + continue: continue_params) + = link_to fork_path, method: :post do + = icon('file fw') + #{ _('Upload file') } + %li + - continue_params = { to: request.fullpath, + notice: edit_in_new_fork_notice + " Try to create a new directory again.", + notice_now: edit_in_new_fork_notice_now } + - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + continue: continue_params) + = link_to fork_path, method: :post do + = icon('folder fw') + #{ _('New directory') } + + %li.divider %li - - continue_params = { to: request.fullpath, - notice: edit_in_new_fork_notice + " Try to upload a file again.", - notice_now: edit_in_new_fork_notice_now } - - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, - continue: continue_params) - = link_to fork_path, method: :post do - = icon('file fw') - #{ _('Upload file') } + = link_to new_namespace_project_branch_path(@project.namespace, @project) do + = icon('code-fork fw') + #{ _('New branch') } %li - - continue_params = { to: request.fullpath, - notice: edit_in_new_fork_notice + " Try to create a new directory again.", - notice_now: edit_in_new_fork_notice_now } - - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, - continue: continue_params) - = link_to fork_path, method: :post do - = icon('folder fw') - #{ _('New directory') } + = link_to new_namespace_project_tag_path(@project.namespace, @project) do + = icon('tags fw') + #{ _('New tag') } + +.tree-controls + = render 'projects/find_file_link' - %li.divider - %li - = link_to new_namespace_project_branch_path(@project.namespace, @project) do - = icon('code-fork fw') - #{ _('New branch') } - %li - = link_to new_namespace_project_tag_path(@project.namespace, @project) do - = icon('tags fw') - #{ _('New tag') } + = link_to s_('Commits|History'), namespace_project_commits_path(@project.namespace, @project, @id), class: 'btn' + + = render 'projects/buttons/download', project: @project, ref: @ref diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index 4b98ff88241..2329de9e11f 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -2,7 +2,7 @@ - nonce = SecureRandom.hex - descriptions = local_assigns.slice(:message_with_description, :message_without_description) = label_tag "commit_message-#{nonce}", class: 'control-label' do - Commit message + #{ _('Commit message') } .col-sm-10 .commit-message-container .max-width-marker diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml index 25a56f84ec5..0a4a24ae807 100644 --- a/app/views/shared/_new_commit_form.html.haml +++ b/app/views/shared/_new_commit_form.html.haml @@ -5,16 +5,12 @@ - else - if can?(current_user, :push_code, @project) .form-group.branch - = label_tag 'branch_name', 'Target branch', class: 'control-label' + = label_tag 'branch_name', _('Target Branch'), class: 'control-label' .col-sm-10 = text_field_tag 'branch_name', @branch_name || tree_edit_branch, required: true, class: "form-control js-branch-name ref-name" .js-create-merge-request-container - .checkbox - - nonce = SecureRandom.hex - = label_tag "create_merge_request-#{nonce}" do - = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" - Start a <strong>new merge request</strong> with these changes + = render 'shared/new_merge_request_checkbox' - else = hidden_field_tag 'branch_name', @branch_name || tree_edit_branch = hidden_field_tag 'create_merge_request', 1 diff --git a/app/views/shared/_new_merge_request_checkbox.html.haml b/app/views/shared/_new_merge_request_checkbox.html.haml new file mode 100644 index 00000000000..133c31f09c4 --- /dev/null +++ b/app/views/shared/_new_merge_request_checkbox.html.haml @@ -0,0 +1,8 @@ +.checkbox + - nonce = SecureRandom.hex + = label_tag "create_merge_request-#{nonce}" do + = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" + - translation_variables = { new_merge_request: "<strong>#{_('new merge request')}</strong>" } + - translation = _('Start a %{new_merge_request} with these changes') % translation_variables + #{ translation.html_safe } + diff --git a/app/views/shared/form_elements/_description.html.haml b/app/views/shared/form_elements/_description.html.haml index 307d4919224..f65bb6a29e6 100644 --- a/app/views/shared/form_elements/_description.html.haml +++ b/app/views/shared/form_elements/_description.html.haml @@ -2,10 +2,10 @@ - model = local_assigns.fetch(:model) - form = local_assigns.fetch(:form) -- supports_slash_commands = model.new_record? +- supports_quick_actions = model.new_record? -- if supports_slash_commands - - preview_url = preview_markdown_path(project, slash_commands_target_type: model.class.name) +- if supports_quick_actions + - preview_url = preview_markdown_path(project, quick_actions_target_type: model.class.name) - else - preview_url = preview_markdown_path(project) @@ -17,7 +17,7 @@ = render 'projects/zen', f: form, attr: :description, classes: 'note-textarea', placeholder: "Write a comment or drag your files here...", - supports_slash_commands: supports_slash_commands - = render 'shared/notes/hints', supports_slash_commands: supports_slash_commands + supports_quick_actions: supports_quick_actions + = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions .clearfix .error-alert diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml index a8a6d84128d..7cfdfb6e6ee 100644 --- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml +++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml @@ -1,7 +1,7 @@ - type = local_assigns.fetch(:type) %aside.issues-bulk-update.js-right-sidebar.right-sidebar.affix-top{ data: { "offset-top" => "50", "spy" => "affix" }, "aria-live" => "polite" } - .issuable-sidebar + .issuable-sidebar.hidden = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: "bulk-update" do .block .filter-item.inline.update-issues-btn.pull-left diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index cf7ba52d840..3f03cc7a275 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -1,24 +1,25 @@ - type = local_assigns.fetch(:type, :issues) - page_context_word = type.to_s.humanize(capitalize: false) - issuables = @issues || @merge_requests -- closed_title = 'Filter by issues that are currently closed.' %ul.nav-links.issues-state-filters %li{ class: active_when(params[:state] == 'opened') }> - %button.btn.btn-link{ id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", type: 'button', data: { state: 'opened' } } + = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do #{issuables_state_counter_text(type, :opened)} - if type == :merge_requests %li{ class: active_when(params[:state] == 'merged') }> - %button.btn.btn-link{ id: 'state-merged', title: 'Filter by merge requests that are currently merged.', type: 'button', data: { state: 'merged' } } + = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.', data: { state: 'merged' } do #{issuables_state_counter_text(type, :merged)} - - closed_title = 'Filter by merge requests that are currently closed and unmerged.' - - %li{ class: active_when(params[:state] == 'closed') }> - %button.btn.btn-link{ id: 'state-closed', title: closed_title, type: 'button', data: { state: 'closed' } } - #{issuables_state_counter_text(type, :closed)} + %li{ class: active_when(params[:state] == 'closed') }> + = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.', data: { state: 'closed' } do + #{issuables_state_counter_text(type, :closed)} + - else + %li{ class: active_when(params[:state] == 'closed') }> + = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed' } do + #{issuables_state_counter_text(type, :closed)} %li{ class: active_when(params[:state] == 'all') }> - %button.btn.btn-link{ id: 'state-all', title: "Show all #{page_context_word}.", type: 'button', data: { state: 'all' } } + = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}.", data: { state: 'all' } do #{issuables_state_counter_text(type, :all)} diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml index 8af3bd597c5..7175e275f95 100644 --- a/app/views/shared/milestones/_issuables.html.haml +++ b/app/views/shared/milestones/_issuables.html.haml @@ -8,11 +8,11 @@ = title - if show_counter .counter - = number_with_delimiter(issuables.size) + = number_with_delimiter(issuables.length) - class_prefix = dom_class(issuables).pluralize - %ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id } + %ul{ class: "well-list milestone-#{class_prefix}-list", id: "#{class_prefix}-list-#{id}" } = render partial: 'shared/milestones/issuable', - collection: issuables.order_position_asc, + collection: issuables, as: :issuable, locals: { show_project_name: show_project_name, show_full_project_name: show_full_project_name } diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index 6a6d817b344..4de8a6cb15f 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -31,12 +31,12 @@ .tab-content.milestone-content - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) .tab-pane.active#tab-issues{ data: { sort_endpoint: (sort_issues_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } } - = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name - .tab-pane#tab-merge-requests{ data: { sort_endpoint: (sort_merge_requests_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } } + = render 'shared/milestones/issues_tab', issues: milestone.sorted_issues(current_user), show_project_name: show_project_name, show_full_project_name: show_full_project_name + .tab-pane#tab-merge-requests -# loaded async = render "shared/milestones/tab_loading" - else - .tab-pane.active#tab-merge-requests{ data: { sort_endpoint: (sort_merge_requests_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } } + .tab-pane.active#tab-merge-requests -# loaded async = render "shared/milestones/tab_loading" .tab-pane#tab-participants diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml index eaf50bc2115..c6b5dcc3647 100644 --- a/app/views/shared/notes/_form.html.haml +++ b/app/views/shared/notes/_form.html.haml @@ -1,6 +1,7 @@ -- supports_slash_commands = note_supports_slash_commands?(@note) -- if supports_slash_commands - - preview_url = preview_markdown_path(@project, slash_commands_target_type: @note.noteable_type, slash_commands_target_id: @note.noteable_id) +- supports_autocomplete = local_assigns.fetch(:supports_autocomplete, true) +- supports_quick_actions = note_supports_quick_actions?(@note) +- if supports_quick_actions + - preview_url = preview_markdown_path(@project, quick_actions_target_type: @note.noteable_type, quick_actions_target_id: @note.noteable_id) - else - preview_url = preview_markdown_path(@project) @@ -27,8 +28,9 @@ attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here...", - supports_slash_commands: supports_slash_commands - = render 'shared/notes/hints', supports_slash_commands: supports_slash_commands + supports_quick_actions: supports_quick_actions, + supports_autocomplete: supports_autocomplete + = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions .error-alert .note-form-actions.clearfix diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml index 7ce6130de60..bc1ac3d8ac2 100644 --- a/app/views/shared/notes/_hints.html.haml +++ b/app/views/shared/notes/_hints.html.haml @@ -1,10 +1,10 @@ -- supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false) +- supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false) .comment-toolbar.clearfix .toolbar-text = link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1 - - if supports_slash_commands + - if supports_quick_actions and - = link_to 'slash commands', help_page_path('user/project/slash_commands'), target: '_blank', tabindex: -1 + = link_to 'quick actions', help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1 are - else is diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml index 5902798dfd0..f0fcc414756 100644 --- a/app/views/shared/notes/_notes_with_form.html.haml +++ b/app/views/shared/notes/_notes_with_form.html.haml @@ -6,13 +6,14 @@ - if can_create_note? %ul.notes.notes-form.timeline %li.timeline-entry - .flash-container.timeline-content + .timeline-entry-inner + .flash-container.timeline-content - .timeline-icon.hidden-xs.hidden-sm - %a.author_link{ href: user_path(current_user) } - = image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40' - .timeline-content.timeline-content-form - = render "shared/notes/form", view: diff_view + .timeline-icon.hidden-xs.hidden-sm + %a.author_link{ href: user_path(current_user) } + = image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40' + .timeline-content.timeline-content-form + = render "shared/notes/form", view: diff_view, supports_autocomplete: autocomplete - elsif !current_user .disabled-comment.text-center.prepend-top-default Please diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb index 79efca4f2f9..48e2da338f6 100644 --- a/app/workers/merge_worker.rb +++ b/app/workers/merge_worker.rb @@ -7,7 +7,7 @@ class MergeWorker current_user = User.find(current_user_id) merge_request = MergeRequest.find(merge_request_id) - MergeRequests::MergeService.new(merge_request.target_project, current_user, params). - execute(merge_request) + MergeRequests::MergeService.new(merge_request.target_project, current_user, params) + .execute(merge_request) end end diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb index fe6a49976e0..c0c03848a40 100644 --- a/app/workers/process_commit_worker.rb +++ b/app/workers/process_commit_worker.rb @@ -47,8 +47,8 @@ class ProcessCommitWorker # therefor we use IssueCollection here and skip the authorization check in # Issues::CloseService#execute. IssueCollection.new(issues).updatable_by_user(user).each do |issue| - Issues::CloseService.new(project, author). - close_issue(issue, commit: commit) + Issues::CloseService.new(project, author) + .close_issue(issue, commit: commit) end end @@ -57,8 +57,8 @@ class ProcessCommitWorker return if mentioned_issues.empty? - Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil). - update_all(first_mentioned_in_commit_at: commit.committed_date) + Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil) + .update_all(first_mentioned_in_commit_at: commit.committed_date) end def build_commit(project, hash) diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index 8ff9d07860f..505ff9e086e 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -32,8 +32,8 @@ class ProjectCacheWorker private def try_obtain_lease_for(project_id, section) - Gitlab::ExclusiveLease. - new("project_cache_worker:#{project_id}:#{section}", timeout: LEASE_TIMEOUT). - try_obtain + Gitlab::ExclusiveLease + .new("project_cache_worker:#{project_id}:#{section}", timeout: LEASE_TIMEOUT) + .try_obtain end end diff --git a/app/workers/propagate_service_template_worker.rb b/app/workers/propagate_service_template_worker.rb index 5ce0e0405d0..6b607451c7a 100644 --- a/app/workers/propagate_service_template_worker.rb +++ b/app/workers/propagate_service_template_worker.rb @@ -14,8 +14,8 @@ class PropagateServiceTemplateWorker private def try_obtain_lease_for(template_id) - Gitlab::ExclusiveLease. - new("propagate_service_template_worker:#{template_id}", timeout: LEASE_TIMEOUT). - try_obtain + Gitlab::ExclusiveLease + .new("propagate_service_template_worker:#{template_id}", timeout: LEASE_TIMEOUT) + .try_obtain end end diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb index 392abb9c21b..2b43bb19ad1 100644 --- a/app/workers/prune_old_events_worker.rb +++ b/app/workers/prune_old_events_worker.rb @@ -10,9 +10,9 @@ class PruneOldEventsWorker '(id IN (SELECT id FROM (?) ids_to_remove))', Event.unscoped.where( 'created_at < ?', - (12.months + 1.day).ago). - select(:id). - limit(10_000)). - delete_all + (12.months + 1.day).ago) + .select(:id) + .limit(10_000)) + .delete_all end end diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb index c3e7491ec4e..b94d83bd709 100644 --- a/app/workers/repository_check/batch_worker.rb +++ b/app/workers/repository_check/batch_worker.rb @@ -32,10 +32,10 @@ module RepositoryCheck # has to sit and wait for this query to finish. def project_ids limit = 10_000 - never_checked_projects = Project.where('last_repository_check_at IS NULL AND created_at < ?', 24.hours.ago). - limit(limit).pluck(:id) - old_check_projects = Project.where('last_repository_check_at < ?', 1.month.ago). - reorder('last_repository_check_at ASC').limit(limit).pluck(:id) + never_checked_projects = Project.where('last_repository_check_at IS NULL AND created_at < ?', 24.hours.ago) + .limit(limit).pluck(:id) + old_check_projects = Project.where('last_repository_check_at < ?', 1.month.ago) + .reorder('last_repository_check_at ASC').limit(limit).pluck(:id) never_checked_projects + old_check_projects end diff --git a/app/workers/update_user_activity_worker.rb b/app/workers/update_user_activity_worker.rb index b3c2f13aa33..31bbdb69edb 100644 --- a/app/workers/update_user_activity_worker.rb +++ b/app/workers/update_user_activity_worker.rb @@ -7,8 +7,8 @@ class UpdateUserActivityWorker ids = pairs.keys conditions = 'WHEN id = ? THEN ? ' * ids.length - User.where(id: ids). - update_all([ + User.where(id: ids) + .update_all([ "last_activity_on = CASE #{conditions} ELSE last_activity_on END", *pairs.to_a.flatten ]) |