diff options
Diffstat (limited to 'app')
235 files changed, 2216 insertions, 2296 deletions
diff --git a/app/assets/images/favicon-blue.ico b/app/assets/images/favicon-blue.ico Binary files differnew file mode 100755 index 00000000000..156fcf07588 --- /dev/null +++ b/app/assets/images/favicon-blue.ico diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index 424dc719c78..aaed74d6073 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -61,4 +61,4 @@ return Admin; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 84bbe90f3b1..86e0ad89431 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -147,4 +147,4 @@ }; window.Api = Api; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 4b5c9686cab..8e468faedbf 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -101,11 +101,6 @@ require('es6-promise').polyfill(); } }); - $('.nav-sidebar').niceScroll({ - cursoropacitymax: '0.4', - cursorcolor: '#FFF', - cursorborder: '1px solid #FFF' - }); $('.js-select-on-focus').on('focusin', function () { return $(this).select().one('mouseup', function (e) { return e.preventDefault(); @@ -245,9 +240,7 @@ require('es6-promise').polyfill(); }); gl.awardsHandler = new AwardsHandler(); new Aside(); - // bind sidebar events - new gl.Sidebar(); gl.utils.initTimeagoTimeout(); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js index 8438de6cdf1..448e6e2cc78 100644 --- a/app/assets/javascripts/aside.js +++ b/app/assets/javascripts/aside.js @@ -22,4 +22,4 @@ return Aside; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index b16a2c0f73a..e55405135fb 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -59,4 +59,4 @@ return Autosave; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 9d776b74965..a4ccb30e447 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -377,4 +377,4 @@ var emojiAliases = require('emoji-aliases'); return AwardsHandler; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index a489523b802..f7f41d55b52 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -25,4 +25,4 @@ var autosize = require('vendor/autosize'); autosize.update($fields); return $fields.css('resize', 'vertical'); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js index 6af8f593872..fd0840fa117 100644 --- a/app/assets/javascripts/behaviors/details_behavior.js +++ b/app/assets/javascripts/behaviors/details_behavior.js @@ -23,4 +23,4 @@ return e.preventDefault(); }); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 7747306688c..a7e68ae5cb9 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -74,4 +74,4 @@ require('../extensions/jquery'); return $this.tooltip('hide'); }); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js index 6276933e93e..6b21695d082 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -59,4 +59,4 @@ require('../extensions/jquery'); return hideOrShowHelpBlock($form); }); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index 04bfe363929..5f14ff40eee 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -63,4 +63,4 @@ return BlobFileDropzone; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js index 1d0bcf6471f..de20eab9cd1 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selector.js +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js @@ -20,4 +20,4 @@ require('./template_selector'); return BlobGitignoreSelector; })(gl.TemplateSelector); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js index 8236457f0f1..43e5c0a5641 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selectors.js +++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js @@ -23,4 +23,4 @@ return BlobGitignoreSelectors; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js index 1d5672d4c48..b582052a76e 100644 --- a/app/assets/javascripts/blob/blob_license_selector.js +++ b/app/assets/javascripts/blob/blob_license_selector.js @@ -25,4 +25,4 @@ require('./template_selector'); return BlobLicenseSelector; })(gl.TemplateSelector); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js index 9e0754819fa..0436bbb0eaf 100644 --- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js @@ -12,4 +12,4 @@ require('./edit_blob'); var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language')); new NewCommitForm($('.js-edit-blob-form')); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index 079445e8278..a1127b9e30e 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -85,4 +85,4 @@ return EditBlob; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 8f30900198e..878ad1b6031 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -95,7 +95,7 @@ $(() => { }, computed: { disabled() { - return Store.shouldAddBlankState(); + return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length; }, }, template: ` diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js index f8dac1ff56e..22e93328548 100644 --- a/app/assets/javascripts/breakpoints.js +++ b/app/assets/javascripts/breakpoints.js @@ -69,4 +69,4 @@ })(this)); window.Breakpoints = Breakpoints; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js index dbdadc73c3f..e8531c43b4b 100644 --- a/app/assets/javascripts/broadcast_message.js +++ b/app/assets/javascripts/broadcast_message.js @@ -31,4 +31,4 @@ } }); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index c5a962dd199..8fa1aceddff 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -275,4 +275,4 @@ return Build; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js index 083448552b6..cae9a0ffca4 100644 --- a/app/assets/javascripts/build_artifacts.js +++ b/app/assets/javascripts/build_artifacts.js @@ -23,4 +23,4 @@ return BuildArtifacts; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js index c656ae4e241..566b322eb49 100644 --- a/app/assets/javascripts/commit.js +++ b/app/assets/javascripts/commit.js @@ -11,4 +11,4 @@ return Commit; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js index 184b4561d2e..ee087c978dd 100644 --- a/app/assets/javascripts/commit/file.js +++ b/app/assets/javascripts/commit/file.js @@ -11,4 +11,4 @@ return CommitFile; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index f09a6b1e676..49bb64a3472 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -173,4 +173,4 @@ return ImageFile; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 index f1b41911b73..f1b80e45444 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 @@ -4,6 +4,7 @@ * * Used to store the Pipelines rendered in the commit view in the pipelines table. */ +require('../../vue_realtime_listener'); class PipelinesStore { constructor() { @@ -24,7 +25,7 @@ class PipelinesStore { * update the time to show how long as passed. * */ - startTimeAgoLoops() { + static startTimeAgoLoops() { const startTimeLoops = () => { this.timeLoopInterval = setInterval(() => { this.$children[0].$children.reduce((acc, component) => { @@ -44,7 +45,4 @@ class PipelinesStore { } } -window.gl = window.gl || {}; -gl.commits = gl.commits || {}; -gl.commits.pipelines = gl.commits.pipelines || {}; -gl.commits.pipelines.PipelinesStore = PipelinesStore; +module.exports = PipelinesStore; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 index 5c1a7eb1052..e7c6c063413 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 @@ -6,9 +6,8 @@ window.Vue.use(require('vue-resource')); require('../../lib/utils/common_utils'); require('../../vue_shared/vue_resource_interceptor'); require('../../vue_shared/components/pipelines_table'); -require('../../vue_realtime_listener/index'); require('./pipelines_service'); -require('./pipelines_store'); +const PipelineStore = require('./pipelines_store'); /** * @@ -41,7 +40,7 @@ require('./pipelines_store'); data() { const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; const svgsData = document.querySelector('.pipeline-svgs').dataset; - const store = new gl.commits.pipelines.PipelinesStore(); + const store = new PipelineStore(); // Transform svgs DOMStringMap to a plain Object. const svgsObject = gl.utils.DOMStringMapToObject(svgsData); @@ -71,7 +70,6 @@ require('./pipelines_store'); .then(response => response.json()) .then((json) => { this.store.storePipelines(json); - this.store.startTimeAgoLoops.call(this, Vue); this.isLoading = false; }) .catch(() => { @@ -80,6 +78,12 @@ require('./pipelines_store'); }); }, + beforeUpdate() { + if (this.state.pipelines.length && this.$children) { + PipelineStore.startTimeAgoLoops.call(this, Vue); + } + }, + template: ` <div class="pipelines"> <div class="realtime-loading" v-if="isLoading"> diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index c6fdfbcaa10..ccd895f3bf4 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -65,4 +65,4 @@ return CommitsList; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js index 9591df70e9c..15df105d4cc 100644 --- a/app/assets/javascripts/compare.js +++ b/app/assets/javascripts/compare.js @@ -88,4 +88,4 @@ return Compare; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6 index 3587431ab69..1eca973e069 100644 --- a/app/assets/javascripts/compare_autocomplete.js.es6 +++ b/app/assets/javascripts/compare_autocomplete.js.es6 @@ -66,4 +66,4 @@ return CompareAutocomplete; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js index 35d98492012..a1c1b721228 100644 --- a/app/assets/javascripts/confirm_danger_modal.js +++ b/app/assets/javascripts/confirm_danger_modal.js @@ -28,4 +28,4 @@ return ConfirmDangerModal; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js index 0029c59e550..615f485e18a 100644 --- a/app/assets/javascripts/copy_to_clipboard.js +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -46,4 +46,4 @@ window.Clipboard = require('vendor/clipboard'); clipboard.on('success', genericSuccess); return clipboard.on('error', genericError); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 index dbdb01c8c68..1ac715aab77 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 @@ -97,7 +97,7 @@ $(() => { } this.isLoadingStage = true; - cycleAnalyticsStore.setStageEvents([]); + cycleAnalyticsStore.setStageEvents([], stage); cycleAnalyticsStore.setActiveStage(stage); cycleAnalyticsService @@ -107,7 +107,7 @@ $(() => { }) .done((response) => { this.isEmptyStage = !response.events.length; - cycleAnalyticsStore.setStageEvents(response.events); + cycleAnalyticsStore.setStageEvents(response.events, stage); }) .error(() => { this.isEmptyStage = true; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 index be732971c7f..3efeb141008 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 @@ -1,4 +1,8 @@ /* eslint-disable no-param-reassign */ + +require('../lib/utils/text_utility'); +const DEFAULT_EVENT_OBJECTS = require('./default_event_objects'); + ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; @@ -34,11 +38,12 @@ }); newData.stages.forEach((item) => { - const stageName = item.title.toLowerCase(); + const stageSlug = gl.text.dasherize(item.title.toLowerCase()); item.active = false; - item.isUserAllowed = data.permissions[stageName]; - item.emptyStageText = EMPTY_STAGE_TEXTS[stageName]; - item.component = `stage-${stageName}-component`; + item.isUserAllowed = data.permissions[stageSlug]; + item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug]; + item.component = `stage-${stageSlug}-component`; + item.slug = stageSlug; }); newData.analytics = data; return newData; @@ -58,31 +63,33 @@ this.deactivateAllStages(); stage.active = true; }, - setStageEvents(events) { - this.state.events = this.decorateEvents(events); + setStageEvents(events, stage) { + this.state.events = this.decorateEvents(events, stage); }, - decorateEvents(events) { + decorateEvents(events, stage) { const newEvents = []; events.forEach((item) => { if (!item) return; - item.totalTime = item.total_time; - item.author.webUrl = item.author.web_url; - item.author.avatarUrl = item.author.avatar_url; + const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item); + + eventItem.totalTime = eventItem.total_time; + eventItem.author.webUrl = eventItem.author.web_url; + eventItem.author.avatarUrl = eventItem.author.avatar_url; - if (item.created_at) item.createdAt = item.created_at; - if (item.short_sha) item.shortSha = item.short_sha; - if (item.commit_url) item.commitUrl = item.commit_url; + if (eventItem.created_at) eventItem.createdAt = eventItem.created_at; + if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha; + if (eventItem.commit_url) eventItem.commitUrl = eventItem.commit_url; - delete item.author.web_url; - delete item.author.avatar_url; - delete item.total_time; - delete item.created_at; - delete item.short_sha; - delete item.commit_url; + delete eventItem.author.web_url; + delete eventItem.author.avatar_url; + delete eventItem.total_time; + delete eventItem.created_at; + delete eventItem.short_sha; + delete eventItem.commit_url; - newEvents.push(item); + newEvents.push(eventItem); }); return newEvents; diff --git a/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 b/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 new file mode 100644 index 00000000000..cfaf9835bf8 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 @@ -0,0 +1,98 @@ +module.exports = { + issue: { + created_at: '', + url: '', + iid: '', + title: '', + total_time: {}, + author: { + avatar_url: '', + id: '', + name: '', + web_url: '', + }, + }, + plan: { + title: '', + commit_url: '', + short_sha: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + }, + code: { + title: '', + iid: '', + created_at: '', + url: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + }, + test: { + name: '', + id: '', + date: '', + url: '', + short_sha: '', + commit_url: '', + total_time: {}, + branch: { + name: '', + url: '', + }, + }, + review: { + title: '', + iid: '', + created_at: '', + url: '', + state: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + }, + staging: { + id: '', + short_sha: '', + date: '', + url: '', + commit_url: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + branch: { + name: '', + url: '', + }, + }, + production: { + title: '', + created_at: '', + url: '', + iid: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + }, +}; diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 7eec2d39a9c..45aa6050aed 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -118,6 +118,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); new gl.IssuableTemplateSelectors(); break; case 'projects:merge_requests:new': + case 'projects:merge_requests:new_diffs': case 'projects:merge_requests:edit': new gl.Diff(); shortcut_handler = new ShortcutsNavigation(); @@ -382,4 +383,4 @@ const ShortcutsBlob = require('./shortcuts_blob'); return Dispatcher; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index a510eebae1a..64a7a9eaf37 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -216,4 +216,4 @@ require('./preview_markdown'); return DropzoneInput; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 91553bda4dc..4b700a39d44 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -1,223 +1,193 @@ /* eslint-disable no-param-reassign, no-new */ -/* global Vue */ -/* global EnvironmentsService */ /* global Flash */ -window.Vue = require('vue'); +const Vue = window.Vue = require('vue'); window.Vue.use(require('vue-resource')); -require('../services/environments_service'); -require('./environment_item'); - -(() => { - window.gl = window.gl || {}; - - gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', { - props: { - store: { - type: Object, - required: true, - default: () => ({}), - }, +const EnvironmentsService = require('../services/environments_service'); +const EnvironmentTable = require('./environments_table'); +const EnvironmentsStore = require('../stores/environments_store'); +require('../../vue_shared/components/table_pagination'); +require('../../lib/utils/common_utils'); +require('../../vue_shared/vue_resource_interceptor'); + +module.exports = Vue.component('environment-component', { + + components: { + 'environment-table': EnvironmentTable, + 'table-pagination': gl.VueGlPagination, + }, + + data() { + const environmentsData = document.querySelector('#environments-list-view').dataset; + const store = new EnvironmentsStore(); + + return { + store, + state: store.state, + visibility: 'available', + isLoading: false, + cssContainerClass: environmentsData.cssClass, + endpoint: environmentsData.environmentsDataEndpoint, + canCreateDeployment: environmentsData.canCreateDeployment, + canReadEnvironment: environmentsData.canReadEnvironment, + canCreateEnvironment: environmentsData.canCreateEnvironment, + projectEnvironmentsPath: environmentsData.projectEnvironmentsPath, + projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath, + newEnvironmentPath: environmentsData.newEnvironmentPath, + helpPagePath: environmentsData.helpPagePath, + commitIconSvg: environmentsData.commitIconSvg, + playIconSvg: environmentsData.playIconSvg, + terminalIconSvg: environmentsData.terminalIconSvg, + + // Pagination Properties, + paginationInformation: {}, + pageNumber: 1, + }; + }, + + computed: { + scope() { + return gl.utils.getParameterByName('scope'); }, - components: { - 'environment-item': gl.environmentsList.EnvironmentItem, + canReadEnvironmentParsed() { + return gl.utils.convertPermissionToBoolean(this.canReadEnvironment); }, - data() { - const environmentsData = document.querySelector('#environments-list-view').dataset; - - return { - state: this.store.state, - visibility: 'available', - isLoading: false, - cssContainerClass: environmentsData.cssClass, - endpoint: environmentsData.environmentsDataEndpoint, - canCreateDeployment: environmentsData.canCreateDeployment, - canReadEnvironment: environmentsData.canReadEnvironment, - canCreateEnvironment: environmentsData.canCreateEnvironment, - projectEnvironmentsPath: environmentsData.projectEnvironmentsPath, - projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath, - newEnvironmentPath: environmentsData.newEnvironmentPath, - helpPagePath: environmentsData.helpPagePath, - commitIconSvg: environmentsData.commitIconSvg, - playIconSvg: environmentsData.playIconSvg, - terminalIconSvg: environmentsData.terminalIconSvg, - }; + canCreateDeploymentParsed() { + return gl.utils.convertPermissionToBoolean(this.canCreateDeployment); }, - computed: { - scope() { - return this.$options.getQueryParameter('scope'); - }, - - canReadEnvironmentParsed() { - return this.$options.convertPermissionToBoolean(this.canReadEnvironment); - }, - - canCreateDeploymentParsed() { - return this.$options.convertPermissionToBoolean(this.canCreateDeployment); - }, - - canCreateEnvironmentParsed() { - return this.$options.convertPermissionToBoolean(this.canCreateEnvironment); - }, + canCreateEnvironmentParsed() { + return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment); }, - /** - * Fetches all the environments and stores them. - * Toggles loading property. - */ - created() { - gl.environmentsService = new EnvironmentsService(this.endpoint); - - const scope = this.$options.getQueryParameter('scope'); - if (scope) { - this.store.storeVisibility(scope); - } - - this.isLoading = true; - - return gl.environmentsService.all() - .then(resp => resp.json()) - .then((json) => { - this.store.storeEnvironments(json); - this.isLoading = false; - }) - .catch(() => { - this.isLoading = false; - new Flash('An error occurred while fetching the environments.', 'alert'); - }); + }, + + /** + * Fetches all the environments and stores them. + * Toggles loading property. + */ + created() { + const scope = gl.utils.getParameterByName('scope') || this.visibility; + const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber; + + const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`; + + const service = new EnvironmentsService(endpoint); + + this.isLoading = true; + + return service.all() + .then(resp => ({ + headers: resp.headers, + body: resp.json(), + })) + .then((response) => { + this.store.storeAvailableCount(response.body.available_count); + this.store.storeStoppedCount(response.body.stopped_count); + this.store.storeEnvironments(response.body.environments); + this.store.setPagination(response.headers); + }) + .then(() => { + this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the environments.', 'alert'); + }); + }, + + methods: { + toggleRow(model) { + return this.store.toggleFolder(model.name); }, /** - * Transforms the url parameter into an object and - * returns the one requested. + * Will change the page number and update the URL. * - * @param {String} param - * @returns {String} The value of the requested parameter. + * @param {Number} pageNumber desired page to go to. + * @return {String} */ - getQueryParameter(parameter) { - return window.location.search.substring(1).split('&').reduce((acc, param) => { - const paramSplited = param.split('='); - acc[paramSplited[0]] = paramSplited[1]; - return acc; - }, {})[parameter]; - }, + changePage(pageNumber) { + const param = gl.utils.setParamInURL('page', pageNumber); - /** - * Converts permission provided as strings to booleans. - * @param {String} string - * @returns {Boolean} - */ - convertPermissionToBoolean(string) { - return string === 'true'; + gl.utils.visitUrl(param); + return param; }, + }, + + template: ` + <div :class="cssContainerClass"> + <div class="top-area"> + <ul v-if="!isLoading" class="nav-links"> + <li v-bind:class="{ 'active': scope === null || scope === 'available' }"> + <a :href="projectEnvironmentsPath"> + Available + <span class="badge js-available-environments-count"> + {{state.availableCounter}} + </span> + </a> + </li> + <li v-bind:class="{ 'active' : scope === 'stopped' }"> + <a :href="projectStoppedEnvironmentsPath"> + Stopped + <span class="badge js-stopped-environments-count"> + {{state.stoppedCounter}} + </span> + </a> + </li> + </ul> + <div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls"> + <a :href="newEnvironmentPath" class="btn btn-create"> + New environment + </a> + </div> + </div> - methods: { - toggleRow(model) { - return this.store.toggleFolder(model.name); - }, - }, + <div class="environments-container"> + <div class="environments-list-loading text-center" v-if="isLoading"> + <i class="fa fa-spinner fa-spin"></i> + </div> - template: ` - <div :class="cssContainerClass"> - <div class="top-area"> - <ul v-if="!isLoading" class="nav-links"> - <li v-bind:class="{ 'active': scope === undefined }"> - <a :href="projectEnvironmentsPath"> - Available - <span class="badge js-available-environments-count"> - {{state.availableCounter}} - </span> - </a> - </li><li v-bind:class="{ 'active' : scope === 'stopped' }"> - <a :href="projectStoppedEnvironmentsPath"> - Stopped - <span class="badge js-stopped-environments-count"> - {{state.stoppedCounter}} - </span> - </a> - </li> - </ul> - <div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls"> - <a :href="newEnvironmentPath" class="btn btn-create"> - New environment + <div class="blank-state blank-state-no-icon" + v-if="!isLoading && state.environments.length === 0"> + <h2 class="blank-state-title js-blank-state-title"> + You don't have any environments right now. + </h2> + <p class="blank-state-text"> + Environments are places where code gets deployed, such as staging or production. + <br /> + <a :href="helpPagePath"> + Read more about environments </a> - </div> + </p> + + <a v-if="canCreateEnvironmentParsed" + :href="newEnvironmentPath" + class="btn btn-create js-new-environment-button"> + New Environment + </a> </div> - <div class="environments-container"> - <div class="environments-list-loading text-center" v-if="isLoading"> - <i class="fa fa-spinner fa-spin"></i> - </div> - - <div class="blank-state blank-state-no-icon" - v-if="!isLoading && state.environments.length === 0"> - <h2 class="blank-state-title js-blank-state-title"> - You don't have any environments right now. - </h2> - <p class="blank-state-text"> - Environments are places where code gets deployed, such as staging or production. - <br /> - <a :href="helpPagePath"> - Read more about environments - </a> - </p> - - <a - v-if="canCreateEnvironmentParsed" - :href="newEnvironmentPath" - class="btn btn-create js-new-environment-button"> - New Environment - </a> - </div> - - <div class="table-holder" - v-if="!isLoading && state.filteredEnvironments.length > 0"> - <table class="table ci-table environments"> - <thead> - <tr> - <th class="environments-name">Environment</th> - <th class="environments-deploy">Last deployment</th> - <th class="environments-build">Job</th> - <th class="environments-commit">Commit</th> - <th class="environments-date">Updated</th> - <th class="hidden-xs environments-actions"></th> - </tr> - </thead> - <tbody> - <template v-for="model in state.filteredEnvironments" - v-bind:model="model"> - - <tr - is="environment-item" - :model="model" - :toggleRow="toggleRow.bind(model)" - :can-create-deployment="canCreateDeploymentParsed" - :can-read-environment="canReadEnvironmentParsed" - :play-icon-svg="playIconSvg" - :terminal-icon-svg="terminalIconSvg" - :commit-icon-svg="commitIconSvg"></tr> - - <tr v-if="model.isOpen && model.children && model.children.length > 0" - is="environment-item" - v-for="children in model.children" - :model="children" - :toggleRow="toggleRow.bind(children)" - :can-create-deployment="canCreateDeploymentParsed" - :can-read-environment="canReadEnvironmentParsed" - :play-icon-svg="playIconSvg" - :terminal-icon-svg="terminalIconSvg" - :commit-icon-svg="commitIconSvg"> - </tr> - - </template> - </tbody> - </table> - </div> + <div class="table-holder" + v-if="!isLoading && state.environments.length > 0"> + + <environment-table + :environments="state.environments" + :can-create-deployment="canCreateDeploymentParsed" + :can-read-environment="canReadEnvironmentParsed" + :play-icon-svg="playIconSvg" + :terminal-icon-svg="terminalIconSvg" + :commit-icon-svg="commitIconSvg"> + </environment-table> + + <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" + :change="changePage" + :pageInfo="state.paginationInformation"> + </table-pagination> </div> </div> - `, - }); -})(); + </div> + `, +}); diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 index ed1c78945db..c5a714d9673 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js.es6 +++ b/app/assets/javascripts/environments/components/environment_actions.js.es6 @@ -1,50 +1,43 @@ -/* global Vue */ - -window.Vue = require('vue'); - -(() => { - window.gl = window.gl || {}; - window.gl.environmentsList = window.gl.environmentsList || {}; - - gl.environmentsList.ActionsComponent = Vue.component('actions-component', { - props: { - actions: { - type: Array, - required: false, - default: () => [], - }, - - playIconSvg: { - type: String, - required: false, - }, +const Vue = require('vue'); + +module.exports = Vue.component('actions-component', { + props: { + actions: { + type: Array, + required: false, + default: () => [], }, - template: ` - <div class="inline"> - <div class="dropdown"> - <a class="dropdown-new btn btn-default" data-toggle="dropdown"> - <span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span> - <i class="fa fa-caret-down"></i> - </a> - - <ul class="dropdown-menu dropdown-menu-align-right"> - <li v-for="action in actions"> - <a :href="action.play_path" - data-method="post" - rel="nofollow" - class="js-manual-action-link"> - - <span class="js-action-play-icon-container" v-html="playIconSvg"></span> - - <span> - {{action.name}} - </span> - </a> - </li> - </ul> - </div> + playIconSvg: { + type: String, + required: false, + }, + }, + + template: ` + <div class="inline"> + <div class="dropdown"> + <a class="dropdown-new btn btn-default" data-toggle="dropdown"> + <span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span> + <i class="fa fa-caret-down"></i> + </a> + + <ul class="dropdown-menu dropdown-menu-align-right"> + <li v-for="action in actions"> + <a :href="action.play_path" + data-method="post" + rel="nofollow" + class="js-manual-action-link"> + + <span class="js-action-play-icon-container" v-html="playIconSvg"></span> + + <span> + {{action.name}} + </span> + </a> + </li> + </ul> </div> - `, - }); -})(); + </div> + `, +}); diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6 index 28cc0022d17..2599bba3c59 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.js.es6 +++ b/app/assets/javascripts/environments/components/environment_external_url.js.es6 @@ -1,23 +1,19 @@ -/* global Vue */ +/** + * Renders the external url link in environments table. + */ +const Vue = require('vue'); -window.Vue = require('vue'); - -(() => { - window.gl = window.gl || {}; - window.gl.environmentsList = window.gl.environmentsList || {}; - - gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', { - props: { - externalUrl: { - type: String, - default: '', - }, +module.exports = Vue.component('external-url-component', { + props: { + externalUrl: { + type: String, + default: '', }, + }, - template: ` - <a class="btn external_url" :href="externalUrl" target="_blank"> - <i class="fa fa-external-link"></i> - </a> - `, - }); -})(); + template: ` + <a class="btn external_url" :href="externalUrl" target="_blank"> + <i class="fa fa-external-link"></i> + </a> + `, +}); diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 39746621c43..24fd58a301a 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -1,538 +1,549 @@ -/* global Vue */ -/* global timeago */ +const Vue = require('vue'); +const Timeago = require('timeago.js'); -window.Vue = require('vue'); -window.timeago = require('timeago.js'); require('../../lib/utils/text_utility'); require('../../vue_shared/components/commit'); -require('./environment_actions'); -require('./environment_external_url'); -require('./environment_stop'); -require('./environment_rollback'); -require('./environment_terminal_button'); +const ActionsComponent = require('./environment_actions'); +const ExternalUrlComponent = require('./environment_external_url'); +const StopComponent = require('./environment_stop'); +const RollbackComponent = require('./environment_rollback'); +const TerminalButtonComponent = require('./environment_terminal_button'); + +/** + * Envrionment Item Component + * + * Renders a table row for each environment. + */ + +const timeagoInstance = new Timeago(); + +module.exports = Vue.component('environment-item', { + + components: { + 'commit-component': gl.CommitComponent, + 'actions-component': ActionsComponent, + 'external-url-component': ExternalUrlComponent, + 'stop-component': StopComponent, + 'rollback-component': RollbackComponent, + 'terminal-button-component': TerminalButtonComponent, + }, + + props: { + model: { + type: Object, + required: true, + default: () => ({}), + }, -(() => { - /** - * Envrionment Item Component - * - * Used in a hierarchical structure to show folders with children - * in a table. - * Recursive component based on [Tree View](https://vuejs.org/examples/tree-view.html) - * - * See this [issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/22539) - * for more information.15 - */ + canCreateDeployment: { + type: Boolean, + required: false, + default: false, + }, - window.gl = window.gl || {}; - window.gl.environmentsList = window.gl.environmentsList || {}; - window.gl.environmentsList.timeagoInstance = new timeago(); // eslint-disable-line - - gl.environmentsList.EnvironmentItem = Vue.component('environment-item', { - - components: { - 'commit-component': gl.CommitComponent, - 'actions-component': gl.environmentsList.ActionsComponent, - 'external-url-component': gl.environmentsList.ExternalUrlComponent, - 'stop-component': gl.environmentsList.StopComponent, - 'rollback-component': gl.environmentsList.RollbackComponent, - 'terminal-button-component': gl.environmentsList.TerminalButtonComponent, - }, - - props: { - model: { - type: Object, - required: true, - default: () => ({}), - }, - - toggleRow: { - type: Function, - required: false, - }, - - canCreateDeployment: { - type: Boolean, - required: false, - default: false, - }, - - canReadEnvironment: { - type: Boolean, - required: false, - default: false, - }, - - commitIconSvg: { - type: String, - required: false, - }, - - playIconSvg: { - type: String, - required: false, - }, - - terminalIconSvg: { - type: String, - required: false, - }, - - }, - - data() { - return { - rowClass: { - 'children-row': this.model['vue-isChildren'], - }, - }; - }, - - computed: { - - /** - * If an item has a `children` entry it means it is a folder. - * Folder items have different behaviours - it is possible to toggle - * them and show their children. - * - * @returns {Boolean|Undefined} - */ - isFolder() { - return this.model.children && this.model.children.length > 0; - }, - - /** - * If an item is inside a folder structure will return true. - * Used for css purposes. - * - * @returns {Boolean|undefined} - */ - isChildren() { - return this.model['vue-isChildren']; - }, - - /** - * Counts the number of environments in each folder. - * Used to show a badge with the counter. - * - * @returns {Number|Undefined} The number of environments for the current folder. - */ - childrenCounter() { - return this.model.children && this.model.children.length; - }, - - /** - * Verifies if `last_deployment` key exists in the current Envrionment. - * This key is required to render most of the html - this method works has - * an helper. - * - * @returns {Boolean} - */ - hasLastDeploymentKey() { - if (this.model.last_deployment && - !this.$options.isObjectEmpty(this.model.last_deployment)) { - return true; - } - return false; - }, - - /** - * Verifies is the given environment has manual actions. - * Used to verify if we should render them or nor. - * - * @returns {Boolean|Undefined} - */ - hasManualActions() { - return this.model.last_deployment && this.model.last_deployment.manual_actions && - this.model.last_deployment.manual_actions.length > 0; - }, - - /** - * Returns the value of the `stop_action?` key provided in the response. - * - * @returns {Boolean} - */ - hasStopAction() { - return this.model['stop_action?']; - }, - - /** - * Verifies if the `deployable` key is present in `last_deployment` key. - * Used to verify whether we should or not render the rollback partial. - * - * @returns {Boolean|Undefined} - */ - canRetry() { - return this.hasLastDeploymentKey && - this.model.last_deployment && - this.model.last_deployment.deployable; - }, - - /** - * Verifies if the date to be shown is present. - * - * @returns {Boolean|Undefined} - */ - canShowDate() { - return this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable !== undefined; - }, - - /** - * Human readable date. - * - * @returns {String} - */ - createdDate() { - return gl.environmentsList.timeagoInstance.format( - this.model.last_deployment.deployable.created_at, - ); - }, - - /** - * Returns the manual actions with the name parsed. - * - * @returns {Array.<Object>|Undefined} - */ - manualActions() { - if (this.hasManualActions) { - return this.model.last_deployment.manual_actions.map((action) => { - const parsedAction = { - name: gl.text.humanize(action.name), - play_path: action.play_path, - }; - return parsedAction; - }); - } - return []; - }, - - /** - * Builds the string used in the user image alt attribute. - * - * @returns {String} - */ - userImageAltDescription() { - if (this.model.last_deployment && - this.model.last_deployment.user && - this.model.last_deployment.user.username) { - return `${this.model.last_deployment.user.username}'s avatar'`; - } - return ''; - }, - - /** - * If provided, returns the commit tag. - * - * @returns {String|Undefined} - */ - commitTag() { - if (this.model.last_deployment && - this.model.last_deployment.tag) { - return this.model.last_deployment.tag; - } - return undefined; - }, - - /** - * If provided, returns the commit ref. - * - * @returns {Object|Undefined} - */ - commitRef() { - if (this.model.last_deployment && this.model.last_deployment.ref) { - return this.model.last_deployment.ref; - } - return undefined; - }, - - /** - * If provided, returns the commit url. - * - * @returns {String|Undefined} - */ - commitUrl() { - if (this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.commit_path) { - return this.model.last_deployment.commit.commit_path; - } - return undefined; - }, - - /** - * If provided, returns the commit short sha. - * - * @returns {String|Undefined} - */ - commitShortSha() { - if (this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.short_id) { - return this.model.last_deployment.commit.short_id; - } - return undefined; - }, - - /** - * If provided, returns the commit title. - * - * @returns {String|Undefined} - */ - commitTitle() { - if (this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.title) { - return this.model.last_deployment.commit.title; - } - return undefined; - }, - - /** - * If provided, returns the commit tag. - * - * @returns {Object|Undefined} - */ - commitAuthor() { - if (this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.author) { - return this.model.last_deployment.commit.author; - } - - return undefined; - }, - - /** - * Verifies if the `retry_path` key is present and returns its value. - * - * @returns {String|Undefined} - */ - retryUrl() { - if (this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.retry_path) { - return this.model.last_deployment.deployable.retry_path; - } - return undefined; - }, - - /** - * Verifies if the `last?` key is present and returns its value. - * - * @returns {Boolean|Undefined} - */ - isLastDeployment() { - return this.model.last_deployment && this.model.last_deployment['last?']; - }, - - /** - * Builds the name of the builds needed to display both the name and the id. - * - * @returns {String} - */ - buildName() { - if (this.model.last_deployment && - this.model.last_deployment.deployable) { - return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`; - } - return ''; - }, - - /** - * Builds the needed string to show the internal id. - * - * @returns {String} - */ - deploymentInternalId() { - if (this.model.last_deployment && - this.model.last_deployment.iid) { - return `#${this.model.last_deployment.iid}`; - } - return ''; - }, - - /** - * Verifies if the user object is present under last_deployment object. - * - * @returns {Boolean} - */ - deploymentHasUser() { - return !this.$options.isObjectEmpty(this.model.last_deployment) && - !this.$options.isObjectEmpty(this.model.last_deployment.user); - }, - - /** - * Returns the user object nested with the last_deployment object. - * Used to render the template. - * - * @returns {Object} - */ - deploymentUser() { - if (!this.$options.isObjectEmpty(this.model.last_deployment) && - !this.$options.isObjectEmpty(this.model.last_deployment.user)) { - return this.model.last_deployment.user; - } - return {}; - }, - - /** - * Verifies if the build name column should be rendered by verifing - * if all the information needed is present - * and if the environment is not a folder. - * - * @returns {Boolean} - */ - shouldRenderBuildName() { - return !this.isFolder && - !this.$options.isObjectEmpty(this.model.last_deployment) && - !this.$options.isObjectEmpty(this.model.last_deployment.deployable); - }, - - /** - * Verifies if deplyment internal ID should be rendered by verifing - * if all the information needed is present - * and if the environment is not a folder. - * - * @returns {Boolean} - */ - shouldRenderDeploymentID() { - return !this.isFolder && - !this.$options.isObjectEmpty(this.model.last_deployment) && - this.model.last_deployment.iid !== undefined; - }, + canReadEnvironment: { + type: Boolean, + required: false, + default: false, + }, + + commitIconSvg: { + type: String, + required: false, + }, + + playIconSvg: { + type: String, + required: false, }, + terminalIconSvg: { + type: String, + required: false, + }, + }, + + computed: { /** - * Helper to verify if certain given object are empty. - * Should be replaced by lodash _.isEmpty - https://lodash.com/docs/4.17.2#isEmpty - * @param {Object} object - * @returns {Bollean} + * Verifies if `last_deployment` key exists in the current Envrionment. + * This key is required to render most of the html - this method works has + * an helper. + * + * @returns {Boolean} */ - isObjectEmpty(object) { - for (const key in object) { // eslint-disable-line - if (hasOwnProperty.call(object, key)) { - return false; - } + hasLastDeploymentKey() { + if (this.model && + this.model.last_deployment && + !this.$options.isObjectEmpty(this.model.last_deployment)) { + return true; } - return true; + return false; }, - template: ` - <tr> - <td v-bind:class="{ 'children-row': isChildren}"> - <a v-if="!isFolder" - class="environment-name" - :href="model.environment_path"> - {{model.name}} - </a> - <span v-else v-on:click="toggleRow(model)" class="folder-name"> - <span class="folder-icon"> - <i v-show="model.isOpen" class="fa fa-caret-down"></i> - <i v-show="!model.isOpen" class="fa fa-caret-right"></i> - </span> - - <span> - {{model.name}} - </span> - - <span class="badge"> - {{childrenCounter}} - </span> - </span> - </td> + /** + * Verifies is the given environment has manual actions. + * Used to verify if we should render them or nor. + * + * @returns {Boolean|Undefined} + */ + hasManualActions() { + return this.model && + this.model.last_deployment && + this.model.last_deployment.manual_actions && + this.model.last_deployment.manual_actions.length > 0; + }, + + /** + * Returns the value of the `stop_action?` key provided in the response. + * + * @returns {Boolean} + */ + hasStopAction() { + return this.model && this.model['stop_action?']; + }, + + /** + * Verifies if the `deployable` key is present in `last_deployment` key. + * Used to verify whether we should or not render the rollback partial. + * + * @returns {Boolean|Undefined} + */ + canRetry() { + return this.model && + this.hasLastDeploymentKey && + this.model.last_deployment && + this.model.last_deployment.deployable; + }, + + /** + * Verifies if the date to be shown is present. + * + * @returns {Boolean|Undefined} + */ + canShowDate() { + return this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable !== undefined; + }, + + /** + * Human readable date. + * + * @returns {String} + */ + createdDate() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.created_at) { + return timeagoInstance.format(this.model.last_deployment.deployable.created_at); + } + return ''; + }, + + /** + * Returns the manual actions with the name parsed. + * + * @returns {Array.<Object>|Undefined} + */ + manualActions() { + if (this.hasManualActions) { + return this.model.last_deployment.manual_actions.map((action) => { + const parsedAction = { + name: gl.text.humanize(action.name), + play_path: action.play_path, + }; + return parsedAction; + }); + } + return []; + }, + + /** + * Builds the string used in the user image alt attribute. + * + * @returns {String} + */ + userImageAltDescription() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.user && + this.model.last_deployment.user.username) { + return `${this.model.last_deployment.user.username}'s avatar'`; + } + return ''; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {String|Undefined} + */ + commitTag() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.tag) { + return this.model.last_deployment.tag; + } + return undefined; + }, + + /** + * If provided, returns the commit ref. + * + * @returns {Object|Undefined} + */ + commitRef() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.ref) { + return this.model.last_deployment.ref; + } + return undefined; + }, + + /** + * If provided, returns the commit url. + * + * @returns {String|Undefined} + */ + commitUrl() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.commit_path) { + return this.model.last_deployment.commit.commit_path; + } + return undefined; + }, + + /** + * If provided, returns the commit short sha. + * + * @returns {String|Undefined} + */ + commitShortSha() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.short_id) { + return this.model.last_deployment.commit.short_id; + } + return undefined; + }, + + /** + * If provided, returns the commit title. + * + * @returns {String|Undefined} + */ + commitTitle() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.title) { + return this.model.last_deployment.commit.title; + } + return undefined; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {Object|Undefined} + */ + commitAuthor() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.author) { + return this.model.last_deployment.commit.author; + } - <td class="deployment-column"> - <span v-if="shouldRenderDeploymentID"> - {{deploymentInternalId}} + return undefined; + }, + + /** + * Verifies if the `retry_path` key is present and returns its value. + * + * @returns {String|Undefined} + */ + retryUrl() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.retry_path) { + return this.model.last_deployment.deployable.retry_path; + } + return undefined; + }, + + /** + * Verifies if the `last?` key is present and returns its value. + * + * @returns {Boolean|Undefined} + */ + isLastDeployment() { + return this.model && this.model.last_deployment && + this.model.last_deployment['last?']; + }, + + /** + * Builds the name of the builds needed to display both the name and the id. + * + * @returns {String} + */ + buildName() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable) { + return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`; + } + return ''; + }, + + /** + * Builds the needed string to show the internal id. + * + * @returns {String} + */ + deploymentInternalId() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.iid) { + return `#${this.model.last_deployment.iid}`; + } + return ''; + }, + + /** + * Verifies if the user object is present under last_deployment object. + * + * @returns {Boolean} + */ + deploymentHasUser() { + return this.model && + !this.$options.isObjectEmpty(this.model.last_deployment) && + !this.$options.isObjectEmpty(this.model.last_deployment.user); + }, + + /** + * Returns the user object nested with the last_deployment object. + * Used to render the template. + * + * @returns {Object} + */ + deploymentUser() { + if (this.model && + !this.$options.isObjectEmpty(this.model.last_deployment) && + !this.$options.isObjectEmpty(this.model.last_deployment.user)) { + return this.model.last_deployment.user; + } + return {}; + }, + + /** + * Verifies if the build name column should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderBuildName() { + return !this.model.isFolder && + !this.$options.isObjectEmpty(this.model.last_deployment) && + !this.$options.isObjectEmpty(this.model.last_deployment.deployable); + }, + + /** + * Verifies the presence of all the keys needed to render the buil_path. + * + * @return {String} + */ + buildPath() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.build_path) { + return this.model.last_deployment.deployable.build_path; + } + + return ''; + }, + + /** + * Verifies the presence of all the keys needed to render the external_url. + * + * @return {String} + */ + externalURL() { + if (this.model && this.model.external_url) { + return this.model.external_url; + } + + return ''; + }, + + /** + * Verifies if deplyment internal ID should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderDeploymentID() { + return !this.model.isFolder && + !this.$options.isObjectEmpty(this.model.last_deployment) && + this.model.last_deployment.iid !== undefined; + }, + + environmentPath() { + if (this.model && this.model.environment_path) { + return this.model.environment_path; + } + + return ''; + }, + + /** + * Constructs folder URL based on the current location and the folder id. + * + * @return {String} + */ + folderUrl() { + return `${window.location.pathname}/folders/${this.model.folderName}`; + }, + + }, + + /** + * Helper to verify if certain given object are empty. + * Should be replaced by lodash _.isEmpty - https://lodash.com/docs/4.17.2#isEmpty + * @param {Object} object + * @returns {Bollean} + */ + isObjectEmpty(object) { + for (const key in object) { // eslint-disable-line + if (hasOwnProperty.call(object, key)) { + return false; + } + } + return true; + }, + + template: ` + <tr> + <td> + <a v-if="!model.isFolder" + class="environment-name" + :href="environmentPath"> + {{model.name}} + </a> + <a v-else class="folder-name" :href="folderUrl"> + <span class="folder-icon"> + <i class="fa fa-folder" aria-hidden="true"></i> </span> - <span v-if="!isFolder && deploymentHasUser"> - by - <a :href="deploymentUser.web_url" class="js-deploy-user-container"> - <img class="avatar has-tooltip s20" - :src="deploymentUser.avatar_url" - :alt="userImageAltDescription" - :title="deploymentUser.username" /> - </a> + <span> + {{model.folderName}} </span> - </td> - <td class="environments-build-cell"> - <a v-if="shouldRenderBuildName" - class="build-link" - :href="model.last_deployment.deployable.build_path"> - {{buildName}} + <span class="badge"> + {{model.size}} + </span> + </a> + </td> + + <td class="deployment-column"> + <span v-if="shouldRenderDeploymentID"> + {{deploymentInternalId}} + </span> + + <span v-if="!model.isFolder && deploymentHasUser"> + by + <a :href="deploymentUser.web_url" class="js-deploy-user-container"> + <img class="avatar has-tooltip s20" + :src="deploymentUser.avatar_url" + :alt="userImageAltDescription" + :title="deploymentUser.username" /> </a> - </td> - - <td> - <div v-if="!isFolder && hasLastDeploymentKey" class="js-commit-component"> - <commit-component - :tag="commitTag" - :commit-ref="commitRef" - :commit-url="commitUrl" - :short-sha="commitShortSha" - :title="commitTitle" - :author="commitAuthor" - :commit-icon-svg="commitIconSvg"> - </commit-component> + </span> + </td> + + <td class="environments-build-cell"> + <a v-if="shouldRenderBuildName" + class="build-link" + :href="buildPath"> + {{buildName}} + </a> + </td> + + <td> + <div v-if="!model.isFolder && hasLastDeploymentKey" class="js-commit-component"> + <commit-component + :tag="commitTag" + :commit-ref="commitRef" + :commit-url="commitUrl" + :short-sha="commitShortSha" + :title="commitTitle" + :author="commitAuthor" + :commit-icon-svg="commitIconSvg"> + </commit-component> + </div> + <p v-if="!model.isFolder && !hasLastDeploymentKey" class="commit-title"> + No deployments yet + </p> + </td> + + <td> + <span v-if="!model.isFolder && canShowDate" + class="environment-created-date-timeago"> + {{createdDate}} + </span> + </td> + + <td class="hidden-xs"> + <div v-if="!model.isFolder"> + <div v-if="hasManualActions && canCreateDeployment" + class="inline js-manual-actions-container"> + <actions-component + :play-icon-svg="playIconSvg" + :actions="manualActions"> + </actions-component> </div> - <p v-if="!isFolder && !hasLastDeploymentKey" class="commit-title"> - No deployments yet - </p> - </td> - - <td> - <span - v-if="!isFolder && canShowDate" - class="environment-created-date-timeago"> - {{createdDate}} - </span> - </td> - - <td class="hidden-xs"> - <div v-if="!isFolder"> - <div v-if="hasManualActions && canCreateDeployment" - class="inline js-manual-actions-container"> - <actions-component - :play-icon-svg="playIconSvg" - :actions="manualActions"> - </actions-component> - </div> - - <div v-if="model.external_url && canReadEnvironment" - class="inline js-external-url-container"> - <external-url-component - :external-url="model.external_url"> - </external-url-component> - </div> - - <div v-if="hasStopAction && canCreateDeployment" - class="inline js-stop-component-container"> - <stop-component - :stop-url="model.stop_path"> - </stop-component> - </div> - - <div v-if="model.terminal_path" - class="inline js-terminal-button-container"> - <terminal-button-component - :terminal-icon-svg="terminalIconSvg" - :terminal-path="model.terminal_path"> - </terminal-button-component> - </div> - - <div v-if="canRetry && canCreateDeployment" - class="inline js-rollback-component-container"> - <rollback-component - :is-last-deployment="isLastDeployment" - :retry-url="retryUrl"> - </rollback-component> - </div> + + <div v-if="externalURL && canReadEnvironment" + class="inline js-external-url-container"> + <external-url-component + :external-url="externalURL"> + </external-url-component> + </div> + + <div v-if="hasStopAction && canCreateDeployment" + class="inline js-stop-component-container"> + <stop-component + :stop-url="model.stop_path"> + </stop-component> + </div> + + <div v-if="model && model.terminal_path" + class="inline js-terminal-button-container"> + <terminal-button-component + :terminal-icon-svg="terminalIconSvg" + :terminal-path="model.terminal_path"> + </terminal-button-component> + </div> + + <div v-if="canRetry && canCreateDeployment" + class="inline js-rollback-component-container"> + <rollback-component + :is-last-deployment="isLastDeployment" + :retry-url="retryUrl"> + </rollback-component> </div> - </td> - </tr> - `, - }); -})(); + </div> + </td> + </tr> + `, +}); diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6 index 5938340a128..daf126eb4e8 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.js.es6 +++ b/app/assets/javascripts/environments/components/environment_rollback.js.es6 @@ -1,33 +1,30 @@ -/* global Vue */ +/** + * Renders Rollback or Re deploy button in environments table depending + * of the provided property `isLastDeployment` + */ +const Vue = require('vue'); -window.Vue = require('vue'); - -(() => { - window.gl = window.gl || {}; - window.gl.environmentsList = window.gl.environmentsList || {}; - - gl.environmentsList.RollbackComponent = Vue.component('rollback-component', { - props: { - retryUrl: { - type: String, - default: '', - }, +module.exports = Vue.component('rollback-component', { + props: { + retryUrl: { + type: String, + default: '', + }, - isLastDeployment: { - type: Boolean, - default: true, - }, + isLastDeployment: { + type: Boolean, + default: true, }, + }, - template: ` - <a class="btn" :href="retryUrl" data-method="post" rel="nofollow"> - <span v-if="isLastDeployment"> - Re-deploy - </span> - <span v-else> - Rollback - </span> - </a> - `, - }); -})(); + template: ` + <a class="btn" :href="retryUrl" data-method="post" rel="nofollow"> + <span v-if="isLastDeployment"> + Re-deploy + </span> + <span v-else> + Rollback + </span> + </a> + `, +}); diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6 index be9526989a0..96983a19568 100644 --- a/app/assets/javascripts/environments/components/environment_stop.js.es6 +++ b/app/assets/javascripts/environments/components/environment_stop.js.es6 @@ -1,27 +1,24 @@ -/* global Vue */ +/** + * Renders the stop "button" that allows stop an environment. + * Used in environments table. + */ +const Vue = require('vue'); -window.Vue = require('vue'); - -(() => { - window.gl = window.gl || {}; - window.gl.environmentsList = window.gl.environmentsList || {}; - - gl.environmentsList.StopComponent = Vue.component('stop-component', { - props: { - stopUrl: { - type: String, - default: '', - }, +module.exports = Vue.component('stop-component', { + props: { + stopUrl: { + type: String, + default: '', }, + }, - template: ` - <a class="btn stop-env-link" - :href="stopUrl" - data-confirm="Are you sure you want to stop this environment?" - data-method="post" - rel="nofollow"> - <i class="fa fa-stop stop-env-icon"></i> - </a> - `, - }); -})(); + template: ` + <a class="btn stop-env-link" + :href="stopUrl" + data-confirm="Are you sure you want to stop this environment?" + data-method="post" + rel="nofollow"> + <i class="fa fa-stop stop-env-icon" aria-hidden="true"></i> + </a> + `, +}); diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 index a3ad063f7cb..481e0d15e7a 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 +++ b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 @@ -1,28 +1,25 @@ -/* global Vue */ +/** + * Renders a terminal button to open a web terminal. + * Used in environments table. + */ +const Vue = require('vue'); -window.Vue = require('vue'); - -(() => { - window.gl = window.gl || {}; - window.gl.environmentsList = window.gl.environmentsList || {}; - - gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', { - props: { - terminalPath: { - type: String, - default: '', - }, - terminalIconSvg: { - type: String, - default: '', - }, +module.exports = Vue.component('terminal-button-component', { + props: { + terminalPath: { + type: String, + default: '', + }, + terminalIconSvg: { + type: String, + default: '', }, + }, - template: ` - <a class="btn terminal-button" - :href="terminalPath"> - <span class="js-terminal-icon-container" v-html="terminalIconSvg"></span> - </a> - `, - }); -})(); + template: ` + <a class="btn terminal-button" + :href="terminalPath"> + <span class="js-terminal-icon-container" v-html="terminalIconSvg"></span> + </a> + `, +}); diff --git a/app/assets/javascripts/environments/components/environments_table.js.es6 b/app/assets/javascripts/environments/components/environments_table.js.es6 new file mode 100644 index 00000000000..fd35d77fd3d --- /dev/null +++ b/app/assets/javascripts/environments/components/environments_table.js.es6 @@ -0,0 +1,74 @@ +/** + * Render environments table. + */ +const Vue = require('vue'); +const EnvironmentItem = require('./environment_item'); + +module.exports = Vue.component('environment-table-component', { + + components: { + 'environment-item': EnvironmentItem, + }, + + props: { + environments: { + type: Array, + required: true, + default: () => ([]), + }, + + canReadEnvironment: { + type: Boolean, + required: false, + default: false, + }, + + canCreateDeployment: { + type: Boolean, + required: false, + default: false, + }, + + commitIconSvg: { + type: String, + required: false, + }, + + playIconSvg: { + type: String, + required: false, + }, + + terminalIconSvg: { + type: String, + required: false, + }, + }, + + template: ` + <table class="table ci-table environments"> + <thead> + <tr> + <th class="environments-name">Environment</th> + <th class="environments-deploy">Last deployment</th> + <th class="environments-build">Job</th> + <th class="environments-commit">Commit</th> + <th class="environments-date">Updated</th> + <th class="hidden-xs environments-actions"></th> + </tr> + </thead> + <tbody> + <template v-for="model in environments" + v-bind:model="model"> + <tr is="environment-item" + :model="model" + :can-create-deployment="canCreateDeployment" + :can-read-environment="canReadEnvironment" + :play-icon-svg="playIconSvg" + :terminal-icon-svg="terminalIconSvg" + :commit-icon-svg="commitIconSvg"></tr> + </template> + </tbody> + </table> + `, +}); diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 05c59d92fd4..7bbba91bc10 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -1,7 +1,4 @@ -window.Vue = require('vue'); -require('./stores/environments_store'); -require('./components/environment'); -require('../vue_shared/vue_resource_interceptor'); +const EnvironmentsComponent = require('./components/environment'); $(() => { window.gl = window.gl || {}; @@ -9,14 +6,8 @@ $(() => { if (gl.EnvironmentsListApp) { gl.EnvironmentsListApp.$destroy(true); } - const Store = gl.environmentsList.EnvironmentsStore; - gl.EnvironmentsListApp = new gl.environmentsList.EnvironmentsComponent({ + gl.EnvironmentsListApp = new EnvironmentsComponent({ el: document.querySelector('#environments-list-view'), - - propsData: { - store: Store.create(), - }, - }); }); diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6 b/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6 new file mode 100644 index 00000000000..d2ca465351a --- /dev/null +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6 @@ -0,0 +1,13 @@ +const EnvironmentsFolderComponent = require('./environments_folder_view'); + +$(() => { + window.gl = window.gl || {}; + + if (gl.EnvironmentsListFolderApp) { + gl.EnvironmentsListFolderApp.$destroy(true); + } + + gl.EnvironmentsListFolderApp = new EnvironmentsFolderComponent({ + el: document.querySelector('#environments-folder-list-view'), + }); +}); diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 b/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 new file mode 100644 index 00000000000..53d52965758 --- /dev/null +++ b/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 @@ -0,0 +1,182 @@ +/* eslint-disable no-param-reassign, no-new */ +/* global Flash */ + +const Vue = window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +const EnvironmentsService = require('../services/environments_service'); +const EnvironmentTable = require('../components/environments_table'); +const EnvironmentsStore = require('../stores/environments_store'); +require('../../vue_shared/components/table_pagination'); +require('../../lib/utils/common_utils'); +require('../../vue_shared/vue_resource_interceptor'); + +module.exports = Vue.component('environment-folder-view', { + + components: { + 'environment-table': EnvironmentTable, + 'table-pagination': gl.VueGlPagination, + }, + + data() { + const environmentsData = document.querySelector('#environments-folder-list-view').dataset; + const store = new EnvironmentsStore(); + const pathname = window.location.pathname; + const endpoint = `${pathname}.json`; + const folderName = pathname.substr(pathname.lastIndexOf('/') + 1); + + return { + store, + folderName, + endpoint, + state: store.state, + visibility: 'available', + isLoading: false, + cssContainerClass: environmentsData.cssClass, + canCreateDeployment: environmentsData.canCreateDeployment, + canReadEnvironment: environmentsData.canReadEnvironment, + + // svgs + commitIconSvg: environmentsData.commitIconSvg, + playIconSvg: environmentsData.playIconSvg, + terminalIconSvg: environmentsData.terminalIconSvg, + + // Pagination Properties, + paginationInformation: {}, + pageNumber: 1, + }; + }, + + computed: { + scope() { + return gl.utils.getParameterByName('scope'); + }, + + canReadEnvironmentParsed() { + return gl.utils.convertPermissionToBoolean(this.canReadEnvironment); + }, + + canCreateDeploymentParsed() { + return gl.utils.convertPermissionToBoolean(this.canCreateDeployment); + }, + + /** + * URL to link in the stopped tab. + * + * @return {String} + */ + stoppedPath() { + return `${window.location.pathname}?scope=stopped`; + }, + + /** + * URL to link in the available tab. + * + * @return {String} + */ + availablePath() { + return window.location.pathname; + }, + }, + + /** + * Fetches all the environments and stores them. + * Toggles loading property. + */ + created() { + const scope = gl.utils.getParameterByName('scope') || this.visibility; + const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber; + + const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`; + + const service = new EnvironmentsService(endpoint); + + this.isLoading = true; + + return service.all() + .then(resp => ({ + headers: resp.headers, + body: resp.json(), + })) + .then((response) => { + this.store.storeAvailableCount(response.body.available_count); + this.store.storeStoppedCount(response.body.stopped_count); + this.store.storeEnvironments(response.body.environments); + this.store.setPagination(response.headers); + }) + .then(() => { + this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the environments.', 'alert'); + }); + }, + + methods: { + /** + * Will change the page number and update the URL. + * + * @param {Number} pageNumber desired page to go to. + */ + changePage(pageNumber) { + const param = gl.utils.setParamInURL('page', pageNumber); + + gl.utils.visitUrl(param); + return param; + }, + }, + + template: ` + <div :class="cssContainerClass"> + <div class="top-area" v-if="!isLoading"> + + <h4 class="js-folder-name environments-folder-name"> + Environments / <b>{{folderName}}</b> + </h4> + + <ul class="nav-links"> + <li v-bind:class="{ 'active': scope === null || scope === 'available' }"> + <a :href="availablePath" class="js-available-environments-folder-tab"> + Available + <span class="badge js-available-environments-count"> + {{state.availableCounter}} + </span> + </a> + </li> + <li v-bind:class="{ 'active' : scope === 'stopped' }"> + <a :href="stoppedPath" class="js-stopped-environments-folder-tab"> + Stopped + <span class="badge js-stopped-environments-count"> + {{state.stoppedCounter}} + </span> + </a> + </li> + </ul> + </div> + + <div class="environments-container"> + <div class="environments-list-loading text-center" v-if="isLoading"> + <i class="fa fa-spinner fa-spin"></i> + </div> + + <div class="table-holder" + v-if="!isLoading && state.environments.length > 0"> + + <environment-table + :environments="state.environments" + :can-create-deployment="canCreateDeploymentParsed" + :can-read-environment="canReadEnvironmentParsed" + :play-icon-svg="playIconSvg" + :terminal-icon-svg="terminalIconSvg" + :commit-icon-svg="commitIconSvg"> + </environment-table> + + <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" + :change="changePage" + :pageInfo="state.paginationInformation"> + </table-pagination> + </div> + </div> + </div> + `, +}); diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6 index fab8d977f58..9cef335868e 100644 --- a/app/assets/javascripts/environments/services/environments_service.js.es6 +++ b/app/assets/javascripts/environments/services/environments_service.js.es6 @@ -1,20 +1,8 @@ -/* globals Vue */ -/* eslint-disable no-unused-vars, no-param-reassign */ +const Vue = require('vue'); class EnvironmentsService { - - constructor(root) { - Vue.http.options.root = root; - - this.environments = Vue.resource(root); - - Vue.http.interceptors.push((request, next) => { - // needed in order to not break the tests. - if ($.rails) { - request.headers['X-CSRF-Token'] = $.rails.csrfToken(); - } - next(); - }); + constructor(endpoint) { + this.environments = Vue.resource(endpoint); } all() { @@ -22,4 +10,4 @@ class EnvironmentsService { } } -window.EnvironmentsService = EnvironmentsService; +module.exports = EnvironmentsService; diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6 index 9b4090100da..15cd9bde08e 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environments_store.js.es6 @@ -1,190 +1,90 @@ -/* eslint-disable no-param-reassign */ -(() => { - window.gl = window.gl || {}; - window.gl.environmentsList = window.gl.environmentsList || {}; - - gl.environmentsList.EnvironmentsStore = { - state: {}, - - create() { - this.state.environments = []; - this.state.stoppedCounter = 0; - this.state.availableCounter = 0; - this.state.visibility = 'available'; - this.state.filteredEnvironments = []; - - return this; - }, - - /** - * In order to display a tree view we need to modify the received - * data in to a tree structure based on `environment_type` - * sorted alphabetically. - * In each children a `vue-` property will be added. This property will be - * used to know if an item is a children mostly for css purposes. This is - * needed because the children row is a fragment instance and therfore does - * not accept non-prop attributes. - * - * - * @example - * it will transform this: - * [ - * { name: "environment", environment_type: "review" }, - * { name: "environment_1", environment_type: null } - * { name: "environment_2, environment_type: "review" } - * ] - * into this: - * [ - * { name: "review", children: - * [ - * { name: "environment", environment_type: "review", vue-isChildren: true}, - * { name: "environment_2", environment_type: "review", vue-isChildren: true} - * ] - * }, - * {name: "environment_1", environment_type: null} - * ] - * - * - * @param {Array} environments List of environments. - * @returns {Array} Tree structured array with the received environments. - */ - storeEnvironments(environments = []) { - this.state.stoppedCounter = this.countByState(environments, 'stopped'); - this.state.availableCounter = this.countByState(environments, 'available'); - - const environmentsTree = environments.reduce((acc, environment) => { - if (environment.environment_type !== null) { - const occurs = acc.filter(element => element.children && - element.name === environment.environment_type); - - environment['vue-isChildren'] = true; - - if (occurs.length) { - acc[acc.indexOf(occurs[0])].children.push(environment); - acc[acc.indexOf(occurs[0])].children.slice().sort(this.sortByName); - } else { - acc.push({ - name: environment.environment_type, - children: [environment], - isOpen: false, - 'vue-isChildren': environment['vue-isChildren'], - }); - } - } else { - acc.push(environment); - } - - return acc; - }, []).slice().sort(this.sortByName); - - this.state.environments = environmentsTree; - - this.filterEnvironmentsByVisibility(this.state.environments); - - return environmentsTree; - }, - - storeVisibility(visibility) { - this.state.visibility = visibility; - }, - /** - * Given the visibility prop provided by the url query parameter and which - * changes according to the active tab we need to filter which environments - * should be visible. - * - * The environments array is a recursive tree structure and we need to filter - * both root level environments and children environments. - * - * In order to acomplish that, both `filterState` and `filterEnvironmentsByVisibility` - * functions work together. - * The first one works as the filter that verifies if the given environment matches - * the given state. - * The second guarantees both root level and children elements are filtered as well. - * - * Given array of environments will return only - * the environments that match the state stored. - * - * @param {Array} array - * @return {Array} - */ - filterEnvironmentsByVisibility(arr) { - const filteredEnvironments = arr.map((item) => { - if (item.children) { - const filteredChildren = this.filterEnvironmentsByVisibility( - item.children, - ).filter(Boolean); - - if (filteredChildren.length) { - item.children = filteredChildren; - return item; - } - } - - return this.filterState(this.state.visibility, item); - }).filter(Boolean); - - this.state.filteredEnvironments = filteredEnvironments; - return filteredEnvironments; - }, - - /** - * Given the state and the environment, - * returns only if the environment state matches the one provided. - * - * @param {String} state - * @param {Object} environment - * @return {Object} - */ - filterState(state, environment) { - return environment.state === state && environment; - }, - - /** - * Toggles folder open property given the environment type. - * - * @param {String} envType - * @return {Array} - */ - toggleFolder(envType) { - const environments = this.state.environments; - - const environmentsCopy = environments.map((env) => { - if (env['vue-isChildren'] && env.name === envType) { - env.isOpen = !env.isOpen; - } - - return env; - }); - - this.state.environments = environmentsCopy; - - return environmentsCopy; - }, - - /** - * Given an array of environments, returns the number of environments - * that have the given state. - * - * @param {Array} environments - * @param {String} state - * @returns {Number} - */ - countByState(environments, state) { - return environments.filter(env => env.state === state).length; - }, - - /** - * Sorts the two objects provided by their name. - * - * @param {Object} a - * @param {Object} b - * @returns {Number} - */ - sortByName(a, b) { - const nameA = a.name.toUpperCase(); - const nameB = b.name.toUpperCase(); - - return nameA < nameB ? -1 : nameA > nameB ? 1 : 0; // eslint-disable-line - }, - }; -})(); +require('~/lib/utils/common_utils'); +/** + * Environments Store. + * + * Stores received environments, count of stopped environments and count of + * available environments. + */ +class EnvironmentsStore { + constructor() { + this.state = {}; + this.state.environments = []; + this.state.stoppedCounter = 0; + this.state.availableCounter = 0; + this.state.paginationInformation = {}; + + return this; + } + + /** + * + * Stores the received environments. + * + * In the main environments endpoint, each environment has the following schema + * { name: String, size: Number, latest: Object } + * In the endpoint to retrieve environments from each folder, the environment does + * not have the `latest` key and the data is all in the root level. + * To avoid doing this check in the view, we store both cases the same by extracting + * what is inside the `latest` key. + * + * If the `size` is bigger than 1, it means it should be rendered as a folder. + * In those cases we add `isFolder` key in order to render it properly. + * + * @param {Array} environments + * @returns {Array} + */ + storeEnvironments(environments = []) { + const filteredEnvironments = environments.map((env) => { + let filtered = {}; + + if (env.size > 1) { + filtered = Object.assign({}, env, { isFolder: true, folderName: env.name }); + } + + if (env.latest) { + filtered = Object.assign(filtered, env, env.latest); + delete filtered.latest; + } else { + filtered = Object.assign(filtered, env); + } + + return filtered; + }); + + this.state.environments = filteredEnvironments; + + return filteredEnvironments; + } + + setPagination(pagination = {}) { + const normalizedHeaders = gl.utils.normalizeHeaders(pagination); + const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders); + + this.state.paginationInformation = paginationInformation; + return paginationInformation; + } + + /** + * Stores the number of available environments. + * + * @param {Number} count = 0 + * @return {Number} + */ + storeAvailableCount(count = 0) { + this.state.availableCounter = count; + return count; + } + + /** + * Stores the number of closed environments. + * + * @param {Number} count = 0 + * @return {Number} + */ + storeStoppedCount(count = 0) { + this.state.stoppedCounter = count; + return count; + } +} + +module.exports = EnvironmentsStore; diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js index d3b58b2707a..1a489b859e8 100644 --- a/app/assets/javascripts/extensions/jquery.js +++ b/app/assets/javascripts/extensions/jquery.js @@ -13,4 +13,4 @@ return $(this).removeAttr('disabled').removeClass('disabled'); } }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 895a872568d..698870d0ce1 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -144,4 +144,4 @@ } }); }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index 249fe23d4cb..730104b89f9 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -39,4 +39,4 @@ return Flash; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 7f1f2a5d278..60d6658dc16 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -83,12 +83,12 @@ _a = decodeURI("%C3%80"); _y = decodeURI("%C3%BF"); - regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])(([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi'); + regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?!" + atSymbolsWithBar + ")((?:[A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi'); match = regexp.exec(subtext); if (match) { - return (match[1] || match[1] === "") ? match[1] : match[2]; + return match[1]; } else { return null; } @@ -103,6 +103,9 @@ this.input.each((i, input) => { 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 ~) + $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); }); }, setupAtWho: function($input) { @@ -377,4 +380,4 @@ (dataToInspect === loadingState || dataToInspect.name === loadingState); } }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 0d618caf350..bf3da8528f0 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -846,4 +846,4 @@ } }); }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js index 2e6da5750de..75a53aae33c 100644 --- a/app/assets/javascripts/graphs/stat_graph.js +++ b/app/assets/javascripts/graphs/stat_graph.js @@ -15,4 +15,4 @@ return StatGraph; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index d06a1a5dae4..bbfb467ad50 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -113,4 +113,4 @@ window.d3 = require('d3'); return ContributorsStatGraph; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js index 241249fae63..228771da4ee 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -273,4 +273,4 @@ window.d3 = require('d3'); return ContributorsAuthorGraph; })(ContributorsGraph); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js index 29c3163328f..7954c583598 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js @@ -135,4 +135,4 @@ } } }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js index 10dfd05fe3c..c5cb273c5b2 100644 --- a/app/assets/javascripts/group_avatar.js +++ b/app/assets/javascripts/group_avatar.js @@ -17,4 +17,4 @@ return GroupAvatar; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index bc88dc2d092..6b937e7fa0f 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -68,4 +68,4 @@ return GroupsSelect; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index 9390136d3d8..34e4a257ff9 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -78,4 +78,4 @@ new window.ImporterStatus(jobsImportPath, importPath); } }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index c77fbb6a1c7..115312d4b83 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -76,4 +76,4 @@ return IssuableContext; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index c7c744ef61f..de184ab2675 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -156,4 +156,4 @@ return IssuableForm; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 6c08b1b8e61..52457f70d90 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -3,7 +3,7 @@ require('./flash'); require('vendor/jquery.waitforimages'); -require('vendor/task_list'); +require('./task_list'); (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; @@ -11,10 +11,16 @@ require('vendor/task_list'); this.Issue = (function() { function Issue() { this.submitNoteForm = bind(this.submitNoteForm, this); - // Prevent duplicate event bindings - this.disableTaskList(); if ($('a.btn-close').length) { - this.initTaskList(); + this.taskList = new gl.TaskList({ + dataType: 'issue', + fieldName: 'description', + selector: '.detail-page-description', + onSuccess: (result) => { + document.querySelector('#task_status').innerText = result.task_status; + document.querySelector('#task_status_short').innerText = result.task_status_short; + } + }); this.initIssueBtnEventListeners(); } this.initMergeRequests(); @@ -22,11 +28,6 @@ require('vendor/task_list'); this.initCanCreateBranch(); } - Issue.prototype.initTaskList = function() { - $('.detail-page-description .js-task-list-container').taskList('enable'); - return $(document).on('tasklist:changed', '.detail-page-description .js-task-list-container', this.updateTaskList); - }; - Issue.prototype.initIssueBtnEventListeners = function() { var _this, issueFailMessage; _this = this; @@ -54,16 +55,19 @@ require('vendor/task_list'); success: function(data, textStatus, jqXHR) { if ('id' in data) { $(document).trigger('issuable:change'); + const currentTotal = Number($('.issue_counter').text()); if (isClose) { $('a.btn-close').addClass('hidden'); $('a.btn-reopen').removeClass('hidden'); $('div.status-box-closed').removeClass('hidden'); $('div.status-box-open').addClass('hidden'); + $('.issue_counter').text(currentTotal - 1); } else { $('a.btn-reopen').addClass('hidden'); $('a.btn-close').removeClass('hidden'); $('div.status-box-closed').addClass('hidden'); $('div.status-box-open').removeClass('hidden'); + $('.issue_counter').text(currentTotal + 1); } } else { new Flash(issueFailMessage, 'alert'); @@ -82,30 +86,6 @@ require('vendor/task_list'); } }; - Issue.prototype.disableTaskList = function() { - $('.detail-page-description .js-task-list-container').taskList('disable'); - return $(document).off('tasklist:changed', '.detail-page-description .js-task-list-container'); - }; - - Issue.prototype.updateTaskList = function() { - var patchData; - patchData = {}; - patchData['issue'] = { - 'description': $('.js-task-list-field', this).val() - }; - return $.ajax({ - type: 'PATCH', - url: $('form.js-issuable-update').attr('action'), - data: patchData, - success: function(issue) { - document.querySelector('#task_status').innerText = issue.task_status; - document.querySelector('#task_status_short').innerText = issue.task_status_short; - } - }); - // TODO (rspeicher): Make the issue description inline-editable like a note so - // that we can re-use its form here - }; - Issue.prototype.initMergeRequests = function() { var $container; $container = $('#merge-requests'); @@ -152,4 +132,4 @@ require('vendor/task_list'); return Issue; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js index 1d6eff11403..b2cfd3ef2a3 100644 --- a/app/assets/javascripts/issue_status_select.js +++ b/app/assets/javascripts/issue_status_select.js @@ -31,4 +31,4 @@ return IssueStatusSelect; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js index 40ad6fc348e..17a3fc1b1e4 100644 --- a/app/assets/javascripts/labels.js +++ b/app/assets/javascripts/labels.js @@ -43,4 +43,4 @@ return Labels; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index e4cf9057e6d..9e2d14c7f87 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -504,4 +504,4 @@ return LabelsSelect; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index 1c0ea317c1a..08ca9e4fa4d 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -44,4 +44,4 @@ } }); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js index 5221f85ba7a..7862c6797c3 100644 --- a/app/assets/javascripts/lib/cropper.js +++ b/app/assets/javascripts/lib/cropper.js @@ -4,4 +4,4 @@ (function() { -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js index 5a9a501efe3..ebe1e2ae98d 100644 --- a/app/assets/javascripts/lib/raphael.js +++ b/app/assets/javascripts/lib/raphael.js @@ -6,4 +6,4 @@ (function() { -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js index ce090a2e4fd..d93c1d0da59 100644 --- a/app/assets/javascripts/lib/utils/animate.js +++ b/app/assets/javascripts/lib/utils/animate.js @@ -46,4 +46,4 @@ return dfd.promise(); }; })(window); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index bcb3a706b51..45a1d90a9d9 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -232,6 +232,21 @@ }; /** + * Parses pagination object string values into numbers. + * + * @param {Object} paginationInformation + * @returns {Object} + */ + w.gl.utils.parseIntPagination = paginationInformation => ({ + perPage: parseInt(paginationInformation['X-PER-PAGE'], 10), + page: parseInt(paginationInformation['X-PAGE'], 10), + total: parseInt(paginationInformation['X-TOTAL'], 10), + totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10), + nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10), + previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10), + }); + + /** * Transforms a DOMStringMap into a plain object. * * @param {DOMStringMap} DOMStringMapObject @@ -241,5 +256,45 @@ acc[element] = DOMStringMapObject[element]; return acc; }, {}); + + /** + * Updates the search parameter of a URL given the parameter and values provided. + * + * If no search params are present we'll add it. + * If param for page is already present, we'll update it + * If there are params but not for the given one, we'll add it at the end. + * Returns the new search parameters. + * + * @param {String} param + * @param {Number|String|Undefined|Null} value + * @return {String} + */ + w.gl.utils.setParamInURL = (param, value) => { + let search; + const locationSearch = window.location.search; + + if (locationSearch.length === 0) { + search = `?${param}=${value}`; + } + + if (locationSearch.indexOf(param) !== -1) { + const regex = new RegExp(param + '=\\d'); + search = locationSearch.replace(regex, `${param}=${value}`); + } + + if (locationSearch.length && locationSearch.indexOf(param) === -1) { + search = `${locationSearch}&${param}=${value}`; + } + + return search; + }; + + /** + * Converts permission provided as strings to booleans. + * + * @param {String} string + * @returns {Boolean} + */ + w.gl.utils.convertPermissionToBoolean = permission => permission === 'true'; })(window); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js.es6 b/app/assets/javascripts/lib/utils/datetime_utility.js.es6 index f41fa15b147..82dcbdc26c8 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js.es6 +++ b/app/assets/javascripts/lib/utils/datetime_utility.js.es6 @@ -123,4 +123,4 @@ window.dateFormat = require('vendor/date.format'); return Math.floor((date2 - date1) / millisecondsPerDay); }; })(window); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js index 6d5979603b9..66f39122a66 100644 --- a/app/assets/javascripts/lib/utils/notify.js +++ b/app/assets/javascripts/lib/utils/notify.js @@ -44,4 +44,4 @@ w.notify = notifyMe; return w.notifyPermissions = notifyPermissions; })(window); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index d9370db0cf2..f755d212b3c 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -1,5 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len */ +require('vendor/latinise'); + (function() { (function(w) { var base; @@ -164,8 +166,14 @@ gl.text.pluralize = function(str, count) { return str + (count > 1 || count === 0 ? 's' : ''); }; - return gl.text.truncate = function(string, maxLength) { + gl.text.truncate = function(string, maxLength) { return string.substr(0, (maxLength - 3)) + '...'; }; + gl.text.dasherize = function(str) { + return str.replace(/[_\s]+/g, '-'); + }; + gl.text.slugify = function(str) { + return str.trim().toLowerCase().latinise(); + }; })(window); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js index 6d813d61601..db62e0be324 100644 --- a/app/assets/javascripts/lib/utils/type_utility.js +++ b/app/assets/javascripts/lib/utils/type_utility.js @@ -12,4 +12,4 @@ return (obj != null) && (obj.constructor === Object); }; })(window); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/lib/utils/url_utility.js.es6 b/app/assets/javascripts/lib/utils/url_utility.js.es6 index a1558b371f0..1bc81d2e4a4 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js.es6 +++ b/app/assets/javascripts/lib/utils/url_utility.js.es6 @@ -83,4 +83,4 @@ document.location.href = url; }; })(window); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index d7137ec63e4..966fcd8ec47 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -179,4 +179,4 @@ require('vendor/jquery.scrollTo'); return LineHighlighter; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js index 1b0d0768db8..729baa2e1a7 100644 --- a/app/assets/javascripts/logo.js +++ b/app/assets/javascripts/logo.js @@ -4,4 +4,4 @@ window.addEventListener('beforeunload', function() { $('.tanuki-logo').addClass('animate'); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/member_expiration_date.js.es6 b/app/assets/javascripts/member_expiration_date.js.es6 index efe7c78a8ec..129d2dc5f0a 100644 --- a/app/assets/javascripts/member_expiration_date.js.es6 +++ b/app/assets/javascripts/member_expiration_date.js.es6 @@ -49,4 +49,4 @@ inputs.each(toggleClearInput); }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index e65378cd610..5e01aacf2ba 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -2,7 +2,7 @@ /* global MergeRequestTabs */ require('vendor/jquery.waitforimages'); -require('vendor/task_list'); +require('./task_list'); require('./merge_request_tabs'); (function() { @@ -24,12 +24,18 @@ require('./merge_request_tabs'); }; })(this)); this.initTabs(); - // Prevent duplicate event bindings - this.disableTaskList(); this.initMRBtnListeners(); this.initCommitMessageListeners(); if ($("a.btn-close").length) { - this.initTaskList(); + this.taskList = new gl.TaskList({ + dataType: 'merge_request', + fieldName: 'description', + selector: '.detail-page-description', + onSuccess: (result) => { + document.querySelector('#task_status').innerText = result.task_status; + document.querySelector('#task_status_short').innerText = result.task_status_short; + } + }); } } @@ -50,11 +56,6 @@ require('./merge_request_tabs'); return this.$('.all-commits').removeClass('hide'); }; - MergeRequest.prototype.initTaskList = function() { - $('.detail-page-description .js-task-list-container').taskList('enable'); - return $(document).on('tasklist:changed', '.detail-page-description .js-task-list-container', this.updateTaskList); - }; - MergeRequest.prototype.initMRBtnListeners = function() { var _this; _this = this; @@ -85,30 +86,6 @@ require('./merge_request_tabs'); } }; - MergeRequest.prototype.disableTaskList = function() { - $('.detail-page-description .js-task-list-container').taskList('disable'); - return $(document).off('tasklist:changed', '.detail-page-description .js-task-list-container'); - }; - - MergeRequest.prototype.updateTaskList = function() { - var patchData; - patchData = {}; - patchData['merge_request'] = { - 'description': $('.js-task-list-field', this).val() - }; - return $.ajax({ - type: 'PATCH', - url: $('form.js-issuable-update').attr('action'), - data: patchData, - success: function(mergeRequest) { - document.querySelector('#task_status').innerText = mergeRequest.task_status; - document.querySelector('#task_status_short').innerText = mergeRequest.task_status_short; - } - }); - // TODO (rspeicher): Make the merge request description inline-editable like a - // note so that we can re-use its form here - }; - MergeRequest.prototype.initCommitMessageListeners = function() { $(document).on('click', 'a.js-with-description-link', function(e) { var textarea = $('textarea.js-commit-message'); @@ -131,4 +108,4 @@ require('./merge_request_tabs'); return MergeRequest; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 69aed77c83d..88f08bbaa34 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -110,7 +110,7 @@ require('./smart_interval'); urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : ''; return window.location.href = window.location.pathname + urlSuffix; } else if (data.merge_error) { - return _this.$widgetBody.html("<h4>" + data.merge_error + "</h4>"); + return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>"); } else { callback = function() { return merge_request_widget.mergeInProgress(deleteSourceBranch); @@ -252,7 +252,6 @@ require('./smart_interval'); $('.ci_widget.ci-error').show(); this.setMergeButtonClass('btn-danger'); } - this.initMiniPipelineGraph(); }; MergeRequestWidget.prototype.showCICoverage = function(coverage) { diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js index 527cdc9b698..9548a98f499 100644 --- a/app/assets/javascripts/merged_buttons.js +++ b/app/assets/javascripts/merged_buttons.js @@ -42,4 +42,4 @@ return MergedButtons; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 051cb9fe5c5..7fbaeec7882 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -172,4 +172,4 @@ return Milestone; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 2f08aa7fe8b..8df1c8e7f94 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -199,4 +199,4 @@ return MilestoneSelect; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 index 919fcd0a07b..2145e531331 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 @@ -28,7 +28,7 @@ * All dropdown events are fired at the .dropdown-menu's parent element. */ bindEvents() { - $(document).on('shown.bs.dropdown', this.container, this.getBuildsList); + $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList); } /** diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index 2ae5617206e..b98e6121967 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -83,4 +83,4 @@ return NamespaceSelects; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index a7ccd03b60c..43dc9838977 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -421,4 +421,4 @@ y: h }); }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js index 37bf6436fd1..8e7027b44e7 100644 --- a/app/assets/javascripts/network/network.js +++ b/app/assets/javascripts/network/network.js @@ -17,4 +17,4 @@ return Network; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js index b4491354472..aae509caa79 100644 --- a/app/assets/javascripts/network/network_bundle.js +++ b/app/assets/javascripts/network/network_bundle.js @@ -19,4 +19,4 @@ requireAll(require.context('.', false, /^\.\/(?!network_bundle).*\.(js|es6)$/)); }); return new ShortcutsNetwork(network_graph.branch_graph); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index 7f763c13b50..cb24f212c66 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -100,4 +100,4 @@ return NewBranchForm; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js index 41eea78a3e6..747f693726e 100644 --- a/app/assets/javascripts/new_commit_form.js +++ b/app/assets/javascripts/new_commit_form.js @@ -30,4 +30,4 @@ return NewCommitForm; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 0464b895d6d..03504255bda 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -11,7 +11,7 @@ require('./dropzone_input'); require('./gfm_auto_complete'); require('vendor/jquery.caret'); // required by jquery.atwho require('vendor/jquery.atwho'); -require('vendor/task_list'); +require('./task_list'); (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; @@ -51,7 +51,11 @@ require('vendor/task_list'); this.addBinding(); this.setPollingInterval(); this.setupMainTargetNoteForm(); - this.initTaskList(); + this.taskList = new gl.TaskList({ + dataType: 'note', + fieldName: 'note', + selector: '.notes' + }); this.collapseLongCommitList(); // We are in the Merge Requests page so we need another edit form for Changes tab @@ -125,8 +129,6 @@ require('vendor/task_list'); $(document).off("keydown", ".js-note-text"); $(document).off('click', '.js-comment-resolve-button'); $(document).off("click", '.system-note-commit-list-toggler'); - $('.note .js-task-list-container').taskList('disable'); - return $(document).off('tasklist:changed', '.note .js-task-list-container'); }; Notes.prototype.keydownNoteText = function(e) { @@ -286,7 +288,7 @@ require('vendor/task_list'); // Update datetime format on the recent note gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false); this.collapseLongCommitList(); - this.initTaskList(); + this.taskList.init(); this.refresh(); return this.updateNotesCount(1); } @@ -863,15 +865,6 @@ require('vendor/task_list'); } }; - Notes.prototype.initTaskList = function() { - this.enableTaskList(); - return $(document).on('tasklist:changed', '.note .js-task-list-container', this.updateTaskList.bind(this)); - }; - - Notes.prototype.enableTaskList = function() { - return $('.note .js-task-list-container').taskList('enable'); - }; - Notes.prototype.putEditFormInPlace = function($el) { var $editForm = $(this.getEditFormSelector($el)); var $note = $el.closest('.note'); @@ -896,17 +889,6 @@ require('vendor/task_list'); $editForm.find('.referenced-users').hide(); }; - Notes.prototype.updateTaskList = function(e) { - var $target = $(e.target); - var $list = $target.closest('.js-task-list-container'); - var $editForm = $(this.getEditFormSelector($target)); - var $note = $list.closest('.note'); - - this.putEditFormInPlace($list); - $editForm.find('#note_note').val($note.find('.original-task-list').val()); - $('form', $list).submit(); - }; - Notes.prototype.updateNotesCount = function(updateCount) { return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount); }; @@ -955,4 +937,4 @@ require('vendor/task_list'); return Notes; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js index 926dc35fee8..838356133cd 100644 --- a/app/assets/javascripts/notifications_dropdown.js +++ b/app/assets/javascripts/notifications_dropdown.js @@ -28,4 +28,4 @@ return NotificationsDropdown; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js index c3d7cc0adfb..5005af90d48 100644 --- a/app/assets/javascripts/notifications_form.js +++ b/app/assets/javascripts/notifications_form.js @@ -54,4 +54,4 @@ return NotificationsForm; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 71719917d0c..7c03c8b72d4 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -126,4 +126,4 @@ return Project; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js index a6d3ba9eb86..aabdfbf65e2 100644 --- a/app/assets/javascripts/project_avatar.js +++ b/app/assets/javascripts/project_avatar.js @@ -17,4 +17,4 @@ return ProjectAvatar; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 04fe84683f3..e01668eabef 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -168,4 +168,4 @@ return ProjectFindFile; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js index 208f25a0e33..47197db39d3 100644 --- a/app/assets/javascripts/project_fork.js +++ b/app/assets/javascripts/project_fork.js @@ -10,4 +10,4 @@ return ProjectFork; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js index d7943959238..08334bf1ec5 100644 --- a/app/assets/javascripts/project_import.js +++ b/app/assets/javascripts/project_import.js @@ -10,4 +10,4 @@ return ProjectImport; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/project_label_subscription.js.es6 b/app/assets/javascripts/project_label_subscription.js.es6 index 8365f7118d5..0a811627600 100644 --- a/app/assets/javascripts/project_label_subscription.js.es6 +++ b/app/assets/javascripts/project_label_subscription.js.es6 @@ -38,13 +38,15 @@ this.$buttons.attr('data-status', newStatus); this.$buttons.find('> span').text(newAction); - for (const button of this.$buttons) { + this.$buttons.map((button) => { const $button = $(button); if ($button.attr('data-original-title')) { $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle'); } - } + + return button; + }); }); } } diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js index 3aa6f6771ce..e9927c1bf51 100644 --- a/app/assets/javascripts/project_new.js +++ b/app/assets/javascripts/project_new.js @@ -101,4 +101,4 @@ return ProjectNew; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 7b5e9953598..f80e765ce30 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -101,4 +101,4 @@ return ProjectSelect; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js index aad130cf267..3a51c1f26ac 100644 --- a/app/assets/javascripts/project_show.js +++ b/app/assets/javascripts/project_show.js @@ -6,6 +6,6 @@ return ProjectShow; })(); -}).call(this); +}).call(window); // I kept class for future diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js index 69a11dfaf39..acdf9b7eb5a 100644 --- a/app/assets/javascripts/projects_list.js +++ b/app/assets/javascripts/projects_list.js @@ -47,4 +47,4 @@ }); } }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js index bdbad93ad04..48cae8a4fa9 100644 --- a/app/assets/javascripts/render_gfm.js +++ b/app/assets/javascripts/render_gfm.js @@ -12,4 +12,4 @@ $(document).on('ready load', function() { return $('body').renderGFM(); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js index 6cef449babf..76c61c001ba 100644 --- a/app/assets/javascripts/render_math.js +++ b/app/assets/javascripts/render_math.js @@ -51,4 +51,4 @@ }); } }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 76a0f993ea0..903862cac6b 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -21,11 +21,16 @@ }; Sidebar.prototype.addEventListeners = function() { + const $document = $(document); + const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight, 10); + 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); - $(document).on('click', '.js-sidebar-toggle', function(e, triggered) { + $(window).on('resize', () => throttledSetSidebarHeight()); + $document.on('scroll', () => throttledSetSidebarHeight()); + $document.on('click', '.js-sidebar-toggle', function(e, triggered) { var $allGutterToggleIcons, $this, $thisIcon; e.preventDefault(); $this = $(this); @@ -191,6 +196,17 @@ } }; + Sidebar.prototype.setSidebarHeight = function() { + const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); + const $rightSidebar = $('.js-right-sidebar'); + const diff = $navHeight - $('body').scrollTop(); + if (diff > 0) { + $rightSidebar.outerHeight($(window).height() - diff); + } else { + $rightSidebar.outerHeight('100%'); + } + }; + Sidebar.prototype.isOpen = function() { return this.sidebar.is('.right-sidebar-expanded'); }; @@ -201,4 +217,4 @@ return Sidebar; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index b1c0dc37b4d..e66418beeab 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -97,4 +97,4 @@ return Search; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index c6d9b007ad1..81766f4bd55 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -97,4 +97,4 @@ } }; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js index 7378b322426..e7baea894f6 100644 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js @@ -37,4 +37,4 @@ require('./shortcuts'); return ShortcutsDashboardNavigation; })(Shortcuts); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js index 36e379d634d..a27ac264a5c 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -35,4 +35,4 @@ require('./shortcuts_navigation'); return ShortcutsFindFile; })(ShortcutsNavigation); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index b841abb754d..fe58e98cee5 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -89,4 +89,4 @@ require('./shortcuts_navigation'); return ShortcutsIssuable; })(ShortcutsNavigation); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index cb5f2c53ea6..542cd586df0 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -65,4 +65,4 @@ require('./shortcuts'); return ShortcutsNavigation; })(Shortcuts); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js index 651957f5325..4c2bf8bf001 100644 --- a/app/assets/javascripts/shortcuts_network.js +++ b/app/assets/javascripts/shortcuts_network.js @@ -25,4 +25,4 @@ require('./shortcuts_navigation'); return ShortcutsNetwork; })(ShortcutsNavigation); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6 deleted file mode 100644 index 33e4b7db681..00000000000 --- a/app/assets/javascripts/sidebar.js.es6 +++ /dev/null @@ -1,111 +0,0 @@ -/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign */ -/* global Cookies */ - -(() => { - const pinnedStateCookie = 'pin_nav'; - const sidebarBreakpoint = 1024; - - const pageSelector = '.page-with-sidebar'; - const navbarSelector = '.navbar-gitlab'; - const sidebarWrapperSelector = '.sidebar-wrapper'; - const sidebarContentSelector = '.nav-sidebar'; - - const pinnedToggleSelector = '.js-nav-pin'; - const sidebarToggleSelector = '.toggle-nav-collapse, .side-nav-toggle'; - - const pinnedPageClass = 'page-sidebar-pinned'; - const expandedPageClass = 'page-sidebar-expanded'; - - const pinnedNavbarClass = 'header-sidebar-pinned'; - const expandedNavbarClass = 'header-sidebar-expanded'; - - class Sidebar { - constructor() { - if (!Sidebar.singleton) { - Sidebar.singleton = this; - Sidebar.singleton.init(); - } - - return Sidebar.singleton; - } - - init() { - this.isPinned = Cookies.get(pinnedStateCookie) === 'true'; - this.isExpanded = ( - window.innerWidth >= sidebarBreakpoint && - $(pageSelector).hasClass(expandedPageClass) - ); - $(window).on('resize', () => this.setSidebarHeight()); - $(document) - .on('click', sidebarToggleSelector, () => this.toggleSidebar()) - .on('click', pinnedToggleSelector, () => this.togglePinnedState()) - .on('click', 'html, body, a, button', (e) => this.handleClickEvent(e)) - .on('DOMContentLoaded', () => this.renderState()) - .on('scroll', () => this.setSidebarHeight()) - .on('todo:toggle', (e, count) => this.updateTodoCount(count)); - this.renderState(); - this.setSidebarHeight(); - } - - handleClickEvent(e) { - if (this.isExpanded && (!this.isPinned || window.innerWidth < sidebarBreakpoint)) { - const $target = $(e.target); - const targetIsToggle = $target.closest(sidebarToggleSelector).length > 0; - const targetIsSidebar = $target.closest(sidebarWrapperSelector).length > 0; - if (!targetIsToggle && (!targetIsSidebar || $target.closest('a'))) { - this.toggleSidebar(); - } - } - } - - updateTodoCount(count) { - $('.js-todos-count').text(gl.text.addDelimiter(count)); - } - - toggleSidebar() { - this.isExpanded = !this.isExpanded; - this.renderState(); - } - - setSidebarHeight() { - const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); - const diff = $navHeight - $('body').scrollTop(); - if (diff > 0) { - $('.js-right-sidebar').outerHeight($(window).height() - diff); - } else { - $('.js-right-sidebar').outerHeight('100%'); - } - } - - togglePinnedState() { - this.isPinned = !this.isPinned; - if (!this.isPinned) { - this.isExpanded = false; - } - Cookies.set(pinnedStateCookie, this.isPinned ? 'true' : 'false', { expires: 3650 }); - this.renderState(); - } - - renderState() { - $(pageSelector) - .toggleClass(pinnedPageClass, this.isPinned && this.isExpanded) - .toggleClass(expandedPageClass, this.isExpanded); - $(navbarSelector) - .toggleClass(pinnedNavbarClass, this.isPinned && this.isExpanded) - .toggleClass(expandedNavbarClass, this.isExpanded); - - const $pinnedToggle = $(pinnedToggleSelector); - const tooltipText = this.isPinned ? 'Unpin navigation' : 'Pin navigation'; - const tooltipState = $pinnedToggle.attr('aria-describedby') && this.isExpanded ? 'show' : 'hide'; - $pinnedToggle.attr('title', tooltipText).tooltip('fixTitle').tooltip(tooltipState); - - if (this.isExpanded) { - const sidebarContent = $(sidebarContentSelector); - setTimeout(() => { sidebarContent.niceScroll().updateScrollBar(); }, 200); - } - } - } - - window.gl = window.gl || {}; - gl.Sidebar = Sidebar; -})(); diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 3ee0c73a8d2..294d087554e 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -95,4 +95,4 @@ } }); }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index 64f9065be42..89822246bb8 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -13,4 +13,4 @@ requireAll(require.context('.', false, /^\.\/(?!snippet_bundle).*\.(js|es6)$/)); $(".snippet-file-content").val(editor.getValue()); }); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index 531fd0e9c32..c75b44cc2fd 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -27,4 +27,4 @@ return Star; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js index 187356f0bf9..8b25f43ffc7 100644 --- a/app/assets/javascripts/subscription_select.js +++ b/app/assets/javascripts/subscription_select.js @@ -31,4 +31,4 @@ return SubscriptionSelect; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js index 115716bff6a..7c063fae045 100644 --- a/app/assets/javascripts/syntax_highlight.js +++ b/app/assets/javascripts/syntax_highlight.js @@ -24,4 +24,4 @@ } } }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js new file mode 100644 index 00000000000..dfe24d1fb33 --- /dev/null +++ b/app/assets/javascripts/task_list.js @@ -0,0 +1,40 @@ +require('vendor/task_list'); + +class TaskList { + constructor(options = {}) { + this.selector = options.selector; + this.dataType = options.dataType; + this.fieldName = options.fieldName; + this.onSuccess = options.onSuccess || (() => {}); + this.init(); + } + + init() { + // Prevent duplicate event bindings + this.disable(); + $(`${this.selector} .js-task-list-container`).taskList('enable'); + $(document).on('tasklist:changed', `${this.selector} .js-task-list-container`, this.update.bind(this)); + } + + disable() { + $(`${this.selector} .js-task-list-container`).taskList('disable'); + $(document).off('tasklist:changed', `${this.selector} .js-task-list-container`); + } + + update(e) { + const $target = $(e.target); + const patchData = {}; + patchData[this.dataType] = { + [this.fieldName]: $target.val(), + }; + return $.ajax({ + type: 'PATCH', + url: $target.data('update-url') || $('form.js-issuable-update').attr('action'), + data: patchData, + success: this.onSuccess, + }); + } +} + +window.gl = window.gl || {}; +window.gl.TaskList = TaskList; diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6 index ded683f2ca1..e9513725d9d 100644 --- a/app/assets/javascripts/todos.js.es6 +++ b/app/assets/javascripts/todos.js.es6 @@ -1,28 +1,34 @@ -/* eslint-disable class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, no-param-reassign, max-len */ +/* eslint-disable class-methods-use-this, no-new, func-names, no-unneeded-ternary, object-shorthand, quote-props, no-param-reassign, max-len */ /* global UsersSelect */ ((global) => { class Todos { - constructor({ el } = {}) { - this.allDoneClicked = this.allDoneClicked.bind(this); - this.doneClicked = this.doneClicked.bind(this); - this.el = el || $('.js-todos-options'); - this.perPage = this.el.data('perPage'); - this.clearListeners(); - this.initBtnListeners(); + constructor() { this.initFilters(); + this.bindEvents(); + + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('beforeunload', this.cleanupWrapper); } - clearListeners() { - $('.done-todo').off('click'); - $('.js-todos-mark-all').off('click'); - return $('.todo').off('click'); + cleanup() { + this.unbindEvents(); + document.removeEventListener('beforeunload', this.cleanupWrapper); } - initBtnListeners() { - $('.done-todo').on('click', this.doneClicked); - $('.js-todos-mark-all').on('click', this.allDoneClicked); - return $('.todo').on('click', this.goToTodoUrl); + unbindEvents() { + $('.js-done-todo, .js-undo-todo').off('click', this.updateStateClickedWrapper); + $('.js-todos-mark-all').off('click', this.allDoneClickedWrapper); + $('.todo').off('click', this.goToTodoUrl); + } + + bindEvents() { + this.updateStateClickedWrapper = this.updateStateClicked.bind(this); + this.allDoneClickedWrapper = this.allDoneClicked.bind(this); + + $('.js-done-todo, .js-undo-todo').on('click', this.updateStateClickedWrapper); + $('.js-todos-mark-all').on('click', this.allDoneClickedWrapper); + $('.todo').on('click', this.goToTodoUrl); } initFilters() { @@ -33,7 +39,7 @@ $('form.filter-form').on('submit', function (event) { event.preventDefault(); - gl.utils.visitUrl(this.action + '&' + $(this).serialize()); + gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`); }); } @@ -44,105 +50,72 @@ filterable: searchFields ? true : false, search: { fields: searchFields }, data: $dropdown.data('data'), - clicked: function() { + clicked: function () { return $dropdown.closest('form.filter-form').submit(); - } + }, }); } - doneClicked(e) { + updateStateClicked(e) { e.preventDefault(); - e.stopImmediatePropagation(); - const $target = $(e.currentTarget); - $target.disable(); - return $.ajax({ + const target = e.target; + target.setAttribute('disabled', ''); + target.classList.add('disabled'); + $.ajax({ type: 'POST', - url: $target.attr('href'), + url: target.getAttribute('href'), dataType: 'json', data: { - '_method': 'delete' + '_method': target.getAttribute('data-method'), }, success: (data) => { - this.redirectIfNeeded(data.count); - this.clearDone($target.closest('li')); - return this.updateBadges(data); - } + this.updateState(target); + this.updateBadges(data); + }, }); } allDoneClicked(e) { e.preventDefault(); - e.stopImmediatePropagation(); const $target = $(e.currentTarget); $target.disable(); - return $.ajax({ + $.ajax({ type: 'POST', url: $target.attr('href'), dataType: 'json', data: { - '_method': 'delete' + '_method': 'delete', }, success: (data) => { $target.remove(); $('.js-todos-all').html('<div class="nothing-here-block">You\'re all done!</div>'); - return this.updateBadges(data); - } + this.updateBadges(data); + }, }); } - clearDone($row) { - const $ul = $row.closest('ul'); - $row.remove(); - if (!$ul.find('li').length) { - return $ul.parents('.panel').remove(); + updateState(target) { + const row = target.closest('li'); + const restoreBtn = row.querySelector('.js-undo-todo'); + const doneBtn = row.querySelector('.js-done-todo'); + + target.removeAttribute('disabled'); + target.classList.remove('disabled'); + target.classList.add('hidden'); + + if (target === doneBtn) { + row.classList.add('done-reversible'); + restoreBtn.classList.remove('hidden'); + } else { + row.classList.remove('done-reversible'); + doneBtn.classList.remove('hidden'); } } updateBadges(data) { $(document).trigger('todo:toggle', data.count); $('.todos-pending .badge').text(data.count); - return $('.todos-done .badge').text(data.done_count); - } - - getTotalPages() { - return this.el.data('totalPages'); - } - - getCurrentPage() { - return this.el.data('currentPage'); - } - - getTodosPerPage() { - return this.el.data('perPage'); - } - - redirectIfNeeded(total) { - const currPages = this.getTotalPages(); - const currPage = this.getCurrentPage(); - - // Refresh if no remaining Todos - if (!total) { - window.location.reload(); - return; - } - // Do nothing if no pagination - if (!currPages) { - return; - } - - const newPages = Math.ceil(total / this.getTodosPerPage()); - let url = location.href; - - if (newPages !== currPages) { - // Redirect to previous page if there's one available - if (currPages > 1 && currPage === currPages) { - const pageParams = { - page: currPages - 1 - }; - url = gl.utils.mergeUrlParams(pageParams, url); - } - return gl.utils.visitUrl(url); - } + $('.todos-done .badge').text(data.done_count); } goToTodoUrl(e) { @@ -159,12 +132,12 @@ if (selected.tagName === 'IMG') { const avatarUrl = selected.parentElement.getAttribute('href'); - return window.open(avatarUrl, windowTarget); + window.open(avatarUrl, windowTarget); } else { - return window.open(todoLink, windowTarget); + window.open(todoLink, windowTarget); } } else { - return gl.utils.visitUrl(todoLink); + gl.utils.visitUrl(todoLink); } } } diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js index b1b35fdbd6c..76a821c7a17 100644 --- a/app/assets/javascripts/tree.js +++ b/app/assets/javascripts/tree.js @@ -65,4 +65,4 @@ return TreeView; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js index 86b459e1866..fd1829efe18 100644 --- a/app/assets/javascripts/u2f/error.js +++ b/app/assets/javascripts/u2f/error.js @@ -24,4 +24,4 @@ return U2FError; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index 69d1ff3a39e..17631f2908d 100644 --- a/app/assets/javascripts/u2f/register.js +++ b/app/assets/javascripts/u2f/register.js @@ -95,4 +95,4 @@ return U2FRegister; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js index 34e88220b12..813d363db00 100644 --- a/app/assets/javascripts/u2f/util.js +++ b/app/assets/javascripts/u2f/util.js @@ -9,4 +9,4 @@ return U2FUtil; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js index 6e40dfdf3d8..5111b260e1c 100644 --- a/app/assets/javascripts/users/calendar.js +++ b/app/assets/javascripts/users/calendar.js @@ -221,4 +221,4 @@ return Calendar; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index d4b24d13299..de33a31b411 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -432,4 +432,4 @@ return UsersSelect; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 index 8106934e864..54e8f977a47 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -62,6 +62,7 @@ <li v-for='artifact in pipeline.details.artifacts'> <a rel="nofollow" + download :href='artifact.path' > <i class="fa fa-download" aria-hidden="true"></i> diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 index e47dc6935d6..0265c00a414 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -5,6 +5,7 @@ window.Vue = require('vue'); require('../vue_shared/components/table_pagination'); require('./store'); require('../vue_shared/components/pipelines_table'); +const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store'); ((gl) => { gl.VuePipelines = Vue.extend({ @@ -28,15 +29,34 @@ require('../vue_shared/components/pipelines_table'); }, props: ['scope', 'store', 'svgs'], created() { - const pagenum = gl.utils.getParameterByName('p'); + const pagenum = gl.utils.getParameterByName('page'); const scope = gl.utils.getParameterByName('scope'); if (pagenum) this.pagenum = pagenum; if (scope) this.apiScope = scope; + this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope); }, + + beforeUpdate() { + if (this.pipelines.length && this.$children) { + CommitPipelinesStoreWithTimeAgo.startTimeAgoLoops.call(this, Vue); + } + }, + methods: { + /** + * Changes the URL according to the pagination component. + * + * If no scope is provided, 'all' is assumed. + * + * Pagination component sends "null" when no scope is provided. + * + * @param {Number} pagenum + * @param {String} apiScope = 'all' + */ change(pagenum, apiScope) { - gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`); + if (!apiScope) apiScope = 'all'; + gl.utils.visitUrl(`?scope=${apiScope}&page=${pagenum}`); }, }, template: ` diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6 index 0ee21f00fdc..909007267b9 100644 --- a/app/assets/javascripts/vue_pipelines_index/store.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6 @@ -1,68 +1,31 @@ /* global gl, Flash */ -/* eslint-disable no-param-reassign, no-underscore-dangle */ -require('../vue_realtime_listener'); +/* eslint-disable no-param-reassign */ ((gl) => { const pageValues = (headers) => { const normalized = gl.utils.normalizeHeaders(headers); - - const paginationInfo = { - perPage: +normalized['X-PER-PAGE'], - page: +normalized['X-PAGE'], - total: +normalized['X-TOTAL'], - totalPages: +normalized['X-TOTAL-PAGES'], - nextPage: +normalized['X-NEXT-PAGE'], - previousPage: +normalized['X-PREV-PAGE'], - }; - + const paginationInfo = gl.utils.parseIntPagination(normalized); return paginationInfo; }; gl.PipelineStore = class { fetchDataLoop(Vue, pageNum, url, apiScope) { this.pageRequest = true; - const updatePipelineNums = (count) => { - const { all } = count; - const running = count.running_or_pending; - document.querySelector('.js-totalbuilds-count').innerHTML = all; - document.querySelector('.js-running-count').innerHTML = running; - }; - - const goFetch = () => - this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`) - .then((response) => { - const pageInfo = pageValues(response.headers); - this.pageInfo = Object.assign({}, this.pageInfo, pageInfo); - - const res = JSON.parse(response.body); - this.count = Object.assign({}, this.count, res.count); - this.pipelines = Object.assign([], this.pipelines, res.pipelines); - - updatePipelineNums(this.count); - this.pageRequest = false; - }, () => { - this.pageRequest = false; - return new Flash('An error occurred while fetching the pipelines, please reload the page again.'); - }); - - goFetch(); - - const startTimeLoops = () => { - this.timeLoopInterval = setInterval(() => { - this.$children[0].$children.reduce((acc, component) => { - const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0]; - acc.push(timeAgoComponent); - return acc; - }, []).forEach(e => e.changeTime()); - }, 10000); - }; - startTimeLoops(); + return this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`) + .then((response) => { + const pageInfo = pageValues(response.headers); + this.pageInfo = Object.assign({}, this.pageInfo, pageInfo); - const removeIntervals = () => clearInterval(this.timeLoopInterval); - const startIntervals = () => startTimeLoops(); + const res = JSON.parse(response.body); + this.count = Object.assign({}, this.count, res.count); + this.pipelines = Object.assign([], this.pipelines, res.pipelines); - gl.VueRealtimeListener(removeIntervals, startIntervals); + this.pageRequest = false; + }, () => { + this.pageRequest = false; + return new Flash('An error occurred while fetching the pipelines, please reload the page again.'); + }); } }; })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_shared/components/commit.js.es6 b/app/assets/javascripts/vue_shared/components/commit.js.es6 index 7f7c18ddeb1..ff88e236829 100644 --- a/app/assets/javascripts/vue_shared/components/commit.js.es6 +++ b/app/assets/javascripts/vue_shared/components/commit.js.es6 @@ -1,4 +1,5 @@ /* global Vue */ +window.Vue = require('vue'); (() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 index 67c6cb73761..d8042a9b7fc 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 +++ b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 @@ -57,9 +57,7 @@ window.Vue = require('vue'); }, methods: { changePage(e) { - let apiScope = gl.utils.getParameterByName('scope'); - - if (!apiScope) apiScope = 'all'; + const apiScope = gl.utils.getParameterByName('scope'); const text = e.target.innerText; const { totalPages, nextPage, previousPage } = this.pageInfo; diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6 index ef99b2e92f0..75fd1394a03 100644 --- a/app/assets/javascripts/wikis.js.es6 +++ b/app/assets/javascripts/wikis.js.es6 @@ -1,14 +1,10 @@ /* eslint-disable no-param-reassign */ /* global Breakpoints */ -require('vendor/latinise'); require('./breakpoints'); require('vendor/jquery.nicescroll'); ((global) => { - const dasherize = str => str.replace(/[_\s]+/g, '-'); - const slugify = str => dasherize(str.trim().toLowerCase().latinise()); - class Wikis { constructor() { this.bp = Breakpoints.get(); @@ -34,7 +30,7 @@ require('vendor/jquery.nicescroll'); if (!this.newWikiForm) return; const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); - const slug = slugify(slugInput.value); + const slug = gl.text.slugify(slugInput.value); if (slug.length > 0) { const wikisPath = slugInput.getAttribute('data-wikis-path'); diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index d9261cda1b1..ce626cf7b46 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -94,4 +94,4 @@ require('mousetrap/plugins/pause/mousetrap-pause'); return ZenMode; })(); -}).call(this); +}).call(window); diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 08f203a1bf6..39cf3b5f8ae 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -19,7 +19,6 @@ @import "framework/flash.scss"; @import "framework/forms.scss"; @import "framework/gfm.scss"; -@import "framework/gitlab-theme.scss"; @import "framework/header.scss"; @import "framework/highlight.scss"; @import "framework/issue_box.scss"; diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 0ca5a9343f7..90935b9616b 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -116,7 +116,7 @@ } .btn, -.side-nav-toggle { +.global-dropdown-toggle { @include transition(background-color, border-color, color, box-shadow); } @@ -140,7 +140,6 @@ a { @include transition(background-color, box-shadow); } -.nav-sidebar a, .dropdown-menu a, .dropdown-menu button, .dropdown-menu-nav a { diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss deleted file mode 100644 index d6566dc4ec9..00000000000 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Styles the GitLab application with a specific color theme - * - * $color-light - - * $color - - * $color-darker - - * $color-dark - - */ -@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) { - .page-with-sidebar { - .toggle-nav-collapse, - .pin-nav-btn { - color: $color-light; - - &:hover { - color: $white-light; - } - } - - .sidebar-wrapper { - background: $color-darker; - } - - .sidebar-action-buttons { - color: $color-light; - background-color: lighten($color-darker, 5%); - } - - .nav-sidebar { - li { - a { - color: $color-light; - - &:hover, - &:focus, - &:active { - background: $color-dark; - } - - i { - color: $color-light; - } - - path, - polygon { - fill: $color-light; - } - - .count { - color: $color-light; - background: $color-dark; - } - - svg { - position: relative; - top: 3px; - } - } - - &.separate-item { - border-top: 1px solid $color; - } - - &.active a { - color: $white-light; - background: $color-dark; - - &.no-highlight { - border: none; - } - - i { - color: $white-light; - } - - path, - polygon { - fill: $white-light; - } - } - } - - .about-gitlab { - color: $color-light; - } - } - } -} - -$theme-charcoal-light: #b9bbbe; -$theme-charcoal: #485157; -$theme-charcoal-dark: #3d454d; -$theme-charcoal-darker: #383f45; - -$theme-blue-light: #becde9; -$theme-blue: #2980b9; -$theme-blue-dark: #1970a9; -$theme-blue-darker: #096099; - -$theme-graphite-light: #ccc; -$theme-graphite: #777; -$theme-graphite-dark: #666; -$theme-graphite-darker: #555; - -$theme-black-light: #979797; -$theme-black: #373737; -$theme-black-dark: #272727; -$theme-black-darker: #222; - -$theme-green-light: #adc; -$theme-green: #019875; -$theme-green-dark: #018865; -$theme-green-darker: #017855; - -$theme-violet-light: #98c; -$theme-violet: #548; -$theme-violet-dark: #436; -$theme-violet-darker: #325; - -body { - &.ui_blue { - @include gitlab-theme($theme-blue-light, $theme-blue, $theme-blue-dark, $theme-blue-darker); - } - - &.ui_charcoal { - @include gitlab-theme($theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark, $theme-charcoal-darker); - } - - &.ui_graphite { - @include gitlab-theme($theme-graphite-light, $theme-graphite, $theme-graphite-dark, $theme-graphite-darker); - } - - &.ui_black { - @include gitlab-theme($theme-black-light, $theme-black, $theme-black-dark, $theme-black-darker); - } - - &.ui_green { - @include gitlab-theme($theme-green-light, $theme-green, $theme-green-dark, $theme-green-darker); - } - - &.ui_violet { - @include gitlab-theme($theme-violet-light, $theme-violet, $theme-violet-dark, $theme-violet-darker); - } -} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 34e010e0e8a..3945a789c82 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -100,23 +100,40 @@ header { } } } + } - .side-nav-toggle { - position: absolute; - left: -10px; - margin: 7px 0; - font-size: 18px; - padding: 6px 10px; - border: none; - background-color: $gray-light; + .global-dropdown { + position: absolute; + left: -10px; - &:hover { - background-color: $white-normal; - color: $gl-header-nav-hover-color; + .badge { + font-size: 11px; + } + + li { + &.active a { + font-weight: bold; } } } + .global-dropdown-toggle { + margin: 7px 0; + font-size: 18px; + padding: 6px 10px; + border: none; + background-color: $gray-light; + + &:hover { + background-color: $white-normal; + } + + &:focus { + outline: none; + background-color: $white-normal; + } + } + .header-content { position: relative; height: $header-height; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 20bcb1eeb23..d09b1c9d7f5 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -1,36 +1,3 @@ -.page-with-sidebar { - padding-bottom: 25px; - transition: padding $sidebar-transition-duration; - - &.page-sidebar-pinned { - .sidebar-wrapper { - box-shadow: none; - } - } - - .sidebar-wrapper { - position: fixed; - top: 0; - bottom: 0; - left: 0; - height: 100%; - width: 0; - overflow: hidden; - transition: width $sidebar-transition-duration; - box-shadow: 2px 0 16px 0 $black-transparent; - } -} - -.sidebar-wrapper { - z-index: 1000; - background: $gray-light; - - .nicescroll-rails-hr { - // TODO: Figure out why nicescroll doesn't hide horizontal bar - display: none!important; - } -} - .content-wrapper { width: 100%; transition: padding $sidebar-transition-duration; @@ -47,105 +14,6 @@ } } -.nav-sidebar { - position: absolute; - top: 50px; - bottom: 0; - width: $sidebar_width; - overflow-y: auto; - overflow-x: hidden; - - &.navbar-collapse { - padding: 0 !important; - } - - li { - &.separate-item { - padding-top: 10px; - margin-top: 10px; - } - - .icon-container { - width: 34px; - display: inline-block; - text-align: center; - } - - a { - padding: 7px $gl-sidebar-padding; - font-size: $gl-font-size; - line-height: 24px; - display: block; - text-decoration: none; - font-weight: normal; - - &:hover, - &:active, - &:focus { - text-decoration: none; - } - - i { - font-size: 16px; - } - - i, - svg { - margin-right: 13px; - } - } - } - - .count { - float: right; - padding: 0 8px; - border-radius: 6px; - } - - .about-gitlab { - padding: 7px $gl-sidebar-padding; - font-size: $gl-font-size; - line-height: 24px; - display: block; - text-decoration: none; - font-weight: normal; - position: absolute; - bottom: 10px; - } -} - -.sidebar-action-buttons { - width: $sidebar_width; - position: absolute; - top: 0; - left: 0; - min-height: 50px; - padding: 5px 0; - font-size: 18px; - line-height: 30px; - - .toggle-nav-collapse { - left: 0; - } - - .pin-nav-btn { - right: 0; - display: none; - - @media (min-width: $sidebar-breakpoint) { - display: block; - } - - .fa { - transition: transform .15s; - - .page-sidebar-pinned & { - transform: rotate(90deg); - } - } - } -} - .nav-header-btn { padding: 10px $gl-sidebar-padding; color: inherit; @@ -161,59 +29,16 @@ } } -.page-sidebar-expanded { - .sidebar-wrapper { - width: $sidebar_width; - } -} - -.page-sidebar-pinned { - .content-wrapper, - .layout-nav { - @media (min-width: $sidebar-breakpoint) { - padding-left: $sidebar_width; - } - } - - .merge-request-tabs-holder.affix { - @media (min-width: $sidebar-breakpoint) { - left: $sidebar_width; - } - } - - &.right-sidebar-expanded { - .line-resolve-all-container { - @media (min-width: $sidebar-breakpoint) { - display: none; - } - } - } -} - -header.header-sidebar-pinned { - @media (min-width: $sidebar-breakpoint) { - padding-left: ($sidebar_width + $gl-padding); - - .side-nav-toggle { - display: none; - } - - .header-content { - padding-left: 0; - } - } -} - .right-sidebar-collapsed { padding-right: 0; @media (min-width: $screen-sm-min) { .content-wrapper { - padding-right: $sidebar_collapsed_width; + padding-right: $gutter_collapsed_width; } .merge-request-tabs-holder.affix { - right: $sidebar_collapsed_width; + right: $gutter_collapsed_width; } } @@ -231,7 +56,7 @@ header.header-sidebar-pinned { @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { &:not(.build-sidebar):not(.wiki-sidebar) { - padding-right: $sidebar_collapsed_width; + padding-right: $gutter_collapsed_width; } } @@ -245,12 +70,12 @@ header.header-sidebar-pinned { } &.with-overlay .merge-request-tabs-holder.affix { - right: $sidebar_collapsed_width; + right: $gutter_collapsed_width; } } &.with-overlay { - padding-right: $sidebar_collapsed_width; + padding-right: $gutter_collapsed_width; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 7809d4866f1..ba0af072716 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -1,8 +1,6 @@ /* * Layout */ -$sidebar_collapsed_width: 62px; -$sidebar_width: 220px; $gutter_collapsed_width: 62px; $gutter_width: 290px; $gutter_inner_width: 250px; @@ -541,4 +539,4 @@ Pipeline Graph */ $stage-hover-bg: #eaf3fc; $stage-hover-border: #d1e7fc; -$action-icon-color: #d6d6d6;
\ No newline at end of file +$action-icon-color: #d6d6d6; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index fd101d43b5b..a24292a7c8c 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -91,7 +91,7 @@ } &.scroll-top { - top: 110px; + top: 10px; } &.scroll-bottom { diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 778ef01430e..181dcb7721f 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -10,6 +10,11 @@ font-size: 34px; } +.environments-folder-name { + font-weight: normal; + padding-top: 20px; +} + @media (max-width: $screen-xs-max) { .environments-container { width: 100%; @@ -110,17 +115,20 @@ } } - .children-row .environment-name { - margin-left: 17px; - margin-right: -17px; - } - .folder-icon { - padding: 0 5px 0 0; + margin-right: 3px; + color: $gl-text-color-secondary; + display: inline-block; + + .fa:nth-child(1) { + margin-right: 3px; + } } .folder-name { cursor: pointer; + color: $gl-text-color-secondary; + display: inline-block; } } @@ -135,4 +143,4 @@ margin-right: 0; } } -}
\ No newline at end of file +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index a53cc27fac9..4426169ef5a 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -253,11 +253,11 @@ display: block; } - width: $sidebar_collapsed_width; + width: $gutter_collapsed_width; padding-top: 0; .block { - width: $sidebar_collapsed_width - 2px; + width: $gutter_collapsed_width - 2px; margin-left: -19px; padding: 15px 0 0; border-bottom: none; diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 3da1150f89b..27c47d36818 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -30,6 +30,26 @@ word-wrap: break-word; } } + + .panel-heading { + line-height: $line-height-base; + padding: 14px 16px; + display: -webkit-flex; + display: flex; + + .title { + -webkit-flex: 1; + -webkit-flex-grow: 1; + flex: 1; + flex-grow: 2; + } + + .counter { + -webkit-flex: 1; + flex: 0; + padding-left: 16px; + } + } } .milestone-summary { diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss index 100ace41f2a..305feaacaa1 100644 --- a/app/assets/stylesheets/pages/profiles/preferences.scss +++ b/app/assets/stylesheets/pages/profiles/preferences.scss @@ -1,42 +1,3 @@ -.application-theme { - label { - margin-right: 20px; - text-align: center; - - .preview { - border-radius: 4px; - - height: 80px; - margin-bottom: 10px; - width: 160px; - - &.ui_blue { - background: $theme-blue; - } - - &.ui_charcoal { - background: $theme-charcoal; - } - - &.ui_graphite { - background: $theme-graphite; - } - - &.ui_black { - background: $theme-black; - } - - &.ui_green { - background: $theme-green; - } - - &.ui_violet { - background: $theme-violet; - } - } - } -} - .syntax-theme { label { margin-right: 20px; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 9a72148f408..6b05e5bb4aa 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -102,6 +102,7 @@ font-size: 24px; font-weight: 400; line-height: 1; + word-wrap: break-word; .fa { margin-left: 2px; diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 0d5604aae69..551a66fbf3a 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -60,6 +60,18 @@ } } +.todos-list > .todo.todo-pending.done-reversible { + background-color: $gray-light; + + &:hover { + border-color: $border-color; + } + + .title { + font-weight: normal; + } +} + .todo-item { .todo-title { @include str-truncated(calc(100% - 174px)); diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 948921efc0b..e4487dbcb87 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -149,7 +149,7 @@ } .commit-actions { - width: 200px; + width: 260px; } } diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss index 0ff3c3f5472..6cc1cc8e263 100644 --- a/app/assets/stylesheets/print.scss +++ b/app/assets/stylesheets/print.scss @@ -31,7 +31,6 @@ nav.navbar-collapse.collapse, .blob-commit-info, .file-title, .file-holder, -.sidebar-wrapper, .nav, .btn, ul.notes-form, diff --git a/app/controllers/admin/background_jobs_controller.rb b/app/controllers/admin/background_jobs_controller.rb index 338496013a0..c09095b9849 100644 --- a/app/controllers/admin/background_jobs_controller.rb +++ b/app/controllers/admin/background_jobs_controller.rb @@ -2,5 +2,6 @@ class Admin::BackgroundJobsController < Admin::ApplicationController def show ps_output, _ = Gitlab::Popen.popen(%W(ps -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command)) @sidekiq_processes = ps_output.split("\n").grep(/sidekiq/) + @concurrency = Sidekiq.options[:concurrency] end end diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index 7345c91f67d..348641e5ecb 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -13,7 +13,7 @@ class Admin::RunnersController < Admin::ApplicationController end def update - if @runner.update_attributes(runner_params) + if Ci::UpdateRunnerService.new(@runner).update(runner_params) respond_to do |format| format.js format.html { redirect_to admin_runner_path(@runner) } @@ -31,7 +31,7 @@ class Admin::RunnersController < Admin::ApplicationController end def resume - if @runner.update_attributes(active: true) + if Ci::UpdateRunnerService.new(@runner).update(active: true) redirect_to admin_runners_path, notice: 'Runner was successfully updated.' else redirect_to admin_runners_path, alert: 'Runner was not updated.' @@ -39,7 +39,7 @@ class Admin::RunnersController < Admin::ApplicationController end def pause - if @runner.update_attributes(active: false) + if Ci::UpdateRunnerService.new(@runner).update(active: false) redirect_to admin_runners_path, notice: 'Runner was successfully updated.' else redirect_to admin_runners_path, alert: 'Runner was not updated.' diff --git a/app/controllers/admin/system_info_controller.rb b/app/controllers/admin/system_info_controller.rb index ca04a17caa1..1330399a836 100644 --- a/app/controllers/admin/system_info_controller.rb +++ b/app/controllers/admin/system_info_controller.rb @@ -21,6 +21,7 @@ class Admin::SystemInfoController < Admin::ApplicationController 'mqueue', 'proc', 'pstore', + 'rpc_pipefs', 'securityfs', 'sysfs', 'tmpfs', diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 1cd50852e89..7ffde71c3b1 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -194,7 +194,6 @@ class Admin::UsersController < Admin::ApplicationController :provider, :remember_me, :skype, - :theme_id, :twitter, :username, :website_url diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index e3933e3d7b1..4e61b0886d8 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -29,6 +29,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController end end + def restore + TodoService.new.mark_todos_as_pending_by_ids([params[:id]], current_user) + + render json: todos_counts + end + private def find_todos diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index a9a06ecc808..0d891ef4004 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -34,7 +34,6 @@ class Profiles::PreferencesController < Profiles::ApplicationController :layout, :dashboard, :project_view, - :theme_id ) end end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index db33b60b229..e2f81b09adc 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -83,7 +83,6 @@ class Projects::ApplicationController < ApplicationController end def apply_diff_view_cookie! - @show_changes_tab = params[:view].present? cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present? end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 0ec8f5bd64a..fed75396d6e 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -9,15 +9,40 @@ class Projects::EnvironmentsController < Projects::ApplicationController before_action :verify_api_request!, only: :terminal_websocket_authorize def index - @scope = params[:scope] - @environments = project.environments.includes(:last_deployment) + @environments = project.environments + .with_state(params[:scope] || :available) respond_to do |format| format.html format.json do - render json: EnvironmentSerializer - .new(project: @project, user: current_user) - .represent(@environments) + render json: { + environments: EnvironmentSerializer + .new(project: @project, user: @current_user) + .with_pagination(request, response) + .within_folders + .represent(@environments), + available_count: project.environments.available.count, + stopped_count: project.environments.stopped.count + } + end + end + end + + def folder + folder_environments = project.environments.where(environment_type: params[:id]) + @environments = folder_environments.with_state(params[:scope] || :available) + + respond_to do |format| + format.html + format.json do + render json: { + environments: EnvironmentSerializer + .new(project: @project, user: @current_user) + .with_pagination(request, response) + .represent(@environments), + available_count: folder_environments.available.count, + stopped_count: folder_environments.stopped.count + } end end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index c3e1760f168..2bf3542d089 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -245,6 +245,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController respond_to do |format| format.html do define_new_vars + @show_changes_tab = true render "new" end format.json do @@ -369,10 +370,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def merge_widget_refresh - if merge_request.in_progress_merge_commit_sha || merge_request.state == 'merged' - @status = :success - elsif merge_request.merge_when_build_succeeds + if merge_request.merge_when_build_succeeds @status = :merge_when_build_succeeds + else + # Only MRs that can be merged end in this action + # MR can be already picked up for merge / merged already or can be waiting for worker to be picked up + # in last case it does not have any special status. Possible error is handled inside widget js function + @status = :success end render 'merge' @@ -613,6 +617,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @labels = LabelsFinder.new(current_user, project_id: @project.id).execute + @show_changes_tab = params[:show_changes].present? + define_pipelines_vars end diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index 74c54037ba9..8b50ea207a5 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -12,7 +12,7 @@ class Projects::RunnersController < Projects::ApplicationController end def update - if @runner.update_attributes(runner_params) + if Ci::UpdateRunnerService.new(@runner).update(runner_params) redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' else render 'edit' @@ -28,7 +28,7 @@ class Projects::RunnersController < Projects::ApplicationController end def resume - if @runner.update_attributes(active: true) + if Ci::UpdateRunnerService.new(@runner).update(active: true) redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' else redirect_to runner_path(@runner), alert: 'Runner was not updated.' @@ -36,7 +36,7 @@ class Projects::RunnersController < Projects::ApplicationController end def pause - if @runner.update_attributes(active: false) + if Ci::UpdateRunnerService.new(@runner).update(active: false) redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' else redirect_to runner_path(@runner), alert: 'Runner was not updated.' diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 1576fc80a6b..206c92fe82a 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -16,6 +16,7 @@ # label_name: string # sort: string # non_archived: boolean +# iids: integer[] # class IssuableFinder NONE = '0' @@ -40,6 +41,7 @@ class IssuableFinder items = by_label(items) items = by_due_date(items) items = by_non_archived(items) + items = by_iids(items) sort(items) end @@ -266,16 +268,11 @@ class IssuableFinder end def by_search(items) - if search - items = - if search =~ iid_pattern - items.where(iid: $~[:iid]) - else - items.full_search(search) - end - end + search ? items.full_search(search) : items + end - items + def by_iids(items) + params[:iids].present? ? items.where(iid: params[:iids]) : items end def sort(items) diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 707eddd4d29..f542f72a386 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -26,10 +26,6 @@ class IssuesFinder < IssuableFinder IssuesFinder.not_restricted_by_confidentiality(current_user) end - def iid_pattern - @iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z} - end - def self.not_restricted_by_confidentiality(user) return Issue.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank? diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 8b82255445e..b76ca389f38 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -20,14 +20,4 @@ class MergeRequestsFinder < IssuableFinder def klass MergeRequest end - - private - - def iid_pattern - @iid_pattern ||= %r{\A[ - #{Regexp.escape(MergeRequest.reference_prefix)} - #{Regexp.escape(Issue.reference_prefix)} - ](?<iid>\d+)\z - }x - end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index bee323993a0..6db813d4a02 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -296,4 +296,13 @@ module ApplicationHelper def page_class "issue-boards-page" if current_controller?(:boards) end + + # Returns active css class when condition returns true + # otherwise returns nil. + # + # Example: + # %li{ class: active_when(params[:filter] == '1') } + def active_when(condition) + 'active' if condition + end end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index e21178c7377..c1523b4dabf 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -1,10 +1,4 @@ module NavHelper - def page_sidebar_class - if pinned_nav? - "page-sidebar-expanded page-sidebar-pinned" - end - end - def page_gutter_class if current_path?('merge_requests#show') || current_path?('merge_requests#diffs') || @@ -32,10 +26,6 @@ module NavHelper class_name = '' class_name << " with-horizontal-nav" if defined?(nav) && nav - if pinned_nav? - class_name << " header-sidebar-expanded header-sidebar-pinned" - end - class_name end @@ -46,8 +36,4 @@ module NavHelper def nav_control_class "nav-control" if current_user end - - def pinned_nav? - cookies[:pin_nav] == 'true' - end end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 7d4d049101a..3286a92a8a7 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -34,6 +34,10 @@ module PageLayoutHelper end end + def favicon + Rails.env.development? ? 'favicon-blue.ico' : 'favicon.ico' + end + def page_image default = image_url('gitlab_logo.png') diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index dd0a4ea03f0..c3a08d76318 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -41,10 +41,6 @@ module PreferencesHelper ] end - def user_application_theme - Gitlab::Themes.for_user(current_user).css_class - end - def user_color_scheme Gitlab::ColorSchemes.for_user(current_user).css_class end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 8c1b076c2d7..e018f8e7c4e 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -62,33 +62,10 @@ module Ci new_build.save end - def retry(build, user = nil) - new_build = Ci::Build.create( - ref: build.ref, - tag: build.tag, - options: build.options, - commands: build.commands, - tag_list: build.tag_list, - project: build.project, - pipeline: build.pipeline, - name: build.name, - allow_failure: build.allow_failure, - stage: build.stage, - stage_idx: build.stage_idx, - trigger_request: build.trigger_request, - yaml_variables: build.yaml_variables, - when: build.when, - user: user, - environment: build.environment, - status_event: 'enqueue' - ) - - MergeRequests::AddTodoWhenBuildFailsService - .new(build.project, nil) - .close(new_build) - - build.pipeline.mark_as_processable_after_stage(build.stage_idx) - new_build + def retry(build, current_user) + Ci::RetryBuildService + .new(build.project, current_user) + .execute(build) end end @@ -136,7 +113,7 @@ module Ci project.builds_enabled? && commands.present? && manual? && skipped? end - def play(current_user = nil) + def play(current_user) # Try to queue a current build if self.enqueue self.update(user: current_user) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index bbc358adb83..dc4590a9923 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -214,21 +214,17 @@ module Ci def cancel_running Gitlab::OptimisticLocking.retry_lock( statuses.cancelable) do |cancelable| - cancelable.each(&:cancel) + cancelable.find_each(&:cancel) end end - def retry_failed(user) - Gitlab::OptimisticLocking.retry_lock( - builds.latest.failed_or_canceled) do |failed_or_canceled| - failed_or_canceled.select(&:retryable?).each do |build| - Ci::Build.retry(build, user) - end - end + def retry_failed(current_user) + Ci::RetryPipelineService.new(project, current_user) + .execute(self) end def mark_as_processable_after_stage(stage_idx) - builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process) + builds.skipped.after_stage(stage_idx).find_each(&:process) end def latest? diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index ed1843ba005..07a086b0aca 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -22,8 +22,6 @@ module Ci scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) } scope :ordered, ->() { order(id: :desc) } - after_save :tick_runner_queue, if: :form_editable_changed? - scope :owned_or_shared, ->(project_id) do joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id') .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) @@ -40,6 +38,8 @@ module Ci acts_as_taggable + after_destroy :cleanup_runner_queue + # Searches for runners matching the given query. # # This method uses ILIKE on PostgreSQL and LIKE on MySQL. @@ -147,14 +147,14 @@ module Ci private - def runner_queue_key - "runner:build_queue:#{self.token}" + def cleanup_runner_queue + Gitlab::Redis.with do |redis| + redis.del(runner_queue_key) + end end - def form_editable_changed? - FORM_EDITABLE.any? do |editable| - public_send("#{editable}_changed?") - end + def runner_queue_key + "runner:build_queue:#{self.token}" end def tag_constraints diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 9547c57b2ae..99a6326309d 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -23,9 +23,6 @@ class CommitStatus < ActiveRecord::Base where(id: max_id.group(:name, :commit_id)) end - scope :retried, -> { where.not(id: latest) } - scope :ordered, -> { order(:name) } - scope :failed_but_allowed, -> do where(allow_failure: true, status: [:failed, :canceled]) end @@ -36,8 +33,11 @@ class CommitStatus < ActiveRecord::Base false, all_state_names - [:failed, :canceled]) end + scope :retried, -> { where.not(id: latest) } + scope :ordered, -> { order(:name) } scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) } scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } + scope :after_stage, -> (index) { where('stage_idx > ?', index) } state_machine :status do event :enqueue do diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 461a38d3e8e..229cbee08c6 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -43,7 +43,7 @@ class Namespace < ActiveRecord::Base after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') } # Save the storage paths before the projects are destroyed to use them on after destroy - before_destroy(prepend: true) { @old_repository_storage_paths = repository_storage_paths } + before_destroy(prepend: true) { prepare_for_destroy } after_destroy :rm_dir scope :root, -> { where('type IS NULL') } @@ -212,6 +212,14 @@ class Namespace < ActiveRecord::Base parent_id_changed? end + def prepare_for_destroy + old_repository_storage_paths + end + + def old_repository_storage_paths + @old_repository_storage_paths ||= repository_storage_paths + end + private def repository_storage_paths @@ -225,7 +233,7 @@ class Namespace < ActiveRecord::Base def rm_dir # Remove the namespace directory in all storages paths used by member projects - @old_repository_storage_paths.each do |repository_storage_path| + old_repository_storage_paths.each do |repository_storage_path| # Move namespace directory into trash. # We will remove it later async new_path = "#{path}+#{id}+deleted" diff --git a/app/models/project.rb b/app/models/project.rb index ed43fc2e575..fc5b1a66910 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -214,6 +214,8 @@ class Project < ActiveRecord::Base # Scopes default_scope { where(pending_delete: false) } + scope :with_deleted, -> { unscope(where: :pending_delete) } + scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb index a03605d01fb..86d271a3f69 100644 --- a/app/models/project_services/chat_message/base_message.rb +++ b/app/models/project_services/chat_message/base_message.rb @@ -30,5 +30,9 @@ module ChatMessage def attachment_color '#345' end + + def link(text, url) + "[#{text}](#{url})" + end end end diff --git a/app/models/project_services/chat_message/build_message.rb b/app/models/project_services/chat_message/build_message.rb index 53e35cb21bf..c776e0a20c4 100644 --- a/app/models/project_services/chat_message/build_message.rb +++ b/app/models/project_services/chat_message/build_message.rb @@ -7,7 +7,11 @@ module ChatMessage attr_reader :project_name attr_reader :project_url attr_reader :user_name + attr_reader :user_url attr_reader :duration + attr_reader :stage + attr_reader :build_id + attr_reader :build_name def initialize(params) @sha = params[:sha] @@ -17,7 +21,11 @@ module ChatMessage @project_url = params[:project_url] @status = params[:commit][:status] @user_name = params[:commit][:author_name] + @user_url = params[:commit][:author_url] @duration = params[:commit][:duration] + @stage = params[:build_stage] + @build_name = params[:build_name] + @build_id = params[:build_id] end def pretext @@ -35,7 +43,19 @@ module ChatMessage private def message - "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}" + "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_link} #{humanized_status} on build #{build_link} of stage #{stage} in #{duration} #{'second'.pluralize(duration)}" + end + + def build_url + "#{project_url}/builds/#{build_id}" + end + + def build_link + link(build_name, build_url) + end + + def user_link + link(user_name, user_url) end def format(string) @@ -64,11 +84,11 @@ module ChatMessage end def branch_link - "[#{ref}](#{branch_url})" + link(ref, branch_url) end def project_link - "[#{project_name}](#{project_url})" + link(project_name, project_url) end def commit_url @@ -76,7 +96,7 @@ module ChatMessage end def commit_link - "[#{Commit.truncate_sha(sha)}](#{commit_url})" + link(Commit.truncate_sha(sha), commit_url) end end end diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb index 14fd64e5332..b96aca47e65 100644 --- a/app/models/project_services/chat_message/issue_message.rb +++ b/app/models/project_services/chat_message/issue_message.rb @@ -55,11 +55,11 @@ module ChatMessage end def project_link - "[#{project_name}](#{project_url})" + link(project_name, project_url) end def issue_link - "[#{issue_title}](#{issue_url})" + link(issue_title, issue_url) end def issue_title diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb index ab5e8b24167..5e5efca7bec 100644 --- a/app/models/project_services/chat_message/merge_message.rb +++ b/app/models/project_services/chat_message/merge_message.rb @@ -42,7 +42,7 @@ module ChatMessage end def project_link - "[#{project_name}](#{project_url})" + link(project_name, project_url) end def merge_request_message @@ -50,7 +50,7 @@ module ChatMessage end def merge_request_link - "[merge request !#{merge_request_id}](#{merge_request_url})" + link("merge request !#{merge_request_id}", merge_request_url) end def merge_request_url diff --git a/app/models/project_services/chat_message/note_message.rb b/app/models/project_services/chat_message/note_message.rb index ca1d7207034..552113bac29 100644 --- a/app/models/project_services/chat_message/note_message.rb +++ b/app/models/project_services/chat_message/note_message.rb @@ -3,10 +3,9 @@ module ChatMessage attr_reader :message attr_reader :user_name attr_reader :project_name - attr_reader :project_link + attr_reader :project_url attr_reader :note attr_reader :note_url - attr_reader :title def initialize(params) params = HashWithIndifferentAccess.new(params) @@ -69,15 +68,15 @@ module ChatMessage end def description_message - [{ text: format(@note), color: attachment_color }] + [{ text: format(note), color: attachment_color }] end def project_link - "[#{@project_name}](#{@project_url})" + link(project_name, project_url) end def commented_on_message(target, title) - @message = "#{@user_name} [commented on #{target}](#{@note_url}) in #{project_link}: *#{title}*" + @message = "#{user_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{title}*" end end end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 9db96347322..d0b991db112 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -160,6 +160,10 @@ class ProjectWiki } end + def repository_storage_path + project.repository_storage_path + end + private def init_repo(path_with_namespace) diff --git a/app/models/user.rb b/app/models/user.rb index ad997ce2b13..f614eb66e1f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -21,7 +21,6 @@ class User < ActiveRecord::Base default_value_for :can_create_team, false default_value_for :hide_no_ssh_key, false default_value_for :hide_no_password, false - default_value_for :theme_id, gitlab_config.default_theme attr_encrypted :otp_secret, key: Gitlab::Application.secrets.otp_key_base, diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb index a559d0850c4..69bf693de8d 100644 --- a/app/serializers/analytics_stage_entity.rb +++ b/app/serializers/analytics_stage_entity.rb @@ -2,6 +2,7 @@ class AnalyticsStageEntity < Grape::Entity include EntityDateHelper expose :title + expose :legend expose :description expose :median, as: :value do |stage| diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb index fe16a3784c4..d0a60f134da 100644 --- a/app/serializers/environment_serializer.rb +++ b/app/serializers/environment_serializer.rb @@ -20,8 +20,6 @@ class EnvironmentSerializer < BaseSerializer end def represent(resource, opts = {}) - resource = @paginator.paginate(resource) if paginated? - if itemized? itemize(resource).map do |item| { name: item.name, @@ -29,6 +27,8 @@ class EnvironmentSerializer < BaseSerializer latest: super(item.latest, opts) } end else + resource = @paginator.paginate(resource) if paginated? + super(resource, opts) end end @@ -36,15 +36,20 @@ class EnvironmentSerializer < BaseSerializer private def itemize(resource) - items = resource.group(:item_name).order('item_name ASC') - .pluck('COALESCE(environment_type, name) AS item_name', - 'COUNT(*) AS environments_count', - 'MAX(id) AS last_environment_id') + items = resource.order('folder_name ASC') + .group('COALESCE(environment_type, name)') + .select('COALESCE(environment_type, name) AS folder_name', + 'COUNT(*) AS size', 'MAX(id) AS last_id') + + # It makes a difference when you call `paginate` method, because + # although `page` is effective at the end, it calls counting methods + # immediately. + items = @paginator.paginate(items) if paginated? - environments = resource.where(id: items.map(&:last)).index_by(&:id) + environments = resource.where(id: items.map(&:last_id)).index_by(&:id) - items.map do |name, size, id| - Item.new(name, size, environments[id]) + items.map do |item| + Item.new(item.folder_name, item.size, environments[item.last_id]) end end end diff --git a/app/services/base_service.rb b/app/services/base_service.rb index 1a2bad77a02..fa45506317e 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -1,4 +1,5 @@ class BaseService + include Gitlab::Allowable include Gitlab::CurrentSettings attr_accessor :project, :current_user, :params @@ -7,10 +8,6 @@ class BaseService @project, @current_user, @params = project, user, params.dup end - def can?(object, action, subject) - Ability.allowed?(object, action, subject) - end - def notification_service NotificationService.new end diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb new file mode 100644 index 00000000000..4b47ee489cf --- /dev/null +++ b/app/services/ci/retry_build_service.rb @@ -0,0 +1,42 @@ +module Ci + class RetryBuildService < ::BaseService + CLONE_ATTRIBUTES = %i[pipeline ref tag options commands tag_list name + allow_failure stage stage_idx trigger_request + yaml_variables when environment coverage_regex] + .freeze + + REJECT_ATTRIBUTES = %i[id status user token coverage trace runner + artifacts_file artifacts_metadata artifacts_size + created_at updated_at started_at finished_at + queued_at erased_by erased_at].freeze + + IGNORE_ATTRIBUTES = %i[trace type lock_version project target_url + deploy job_id description].freeze + + def execute(build) + reprocess(build).tap do |new_build| + build.pipeline.mark_as_processable_after_stage(build.stage_idx) + + new_build.enqueue! + + MergeRequests::AddTodoWhenBuildFailsService + .new(project, current_user) + .close(new_build) + end + end + + def reprocess(build) + unless can?(current_user, :update_build, build) + raise Gitlab::Access::AccessDeniedError + end + + attributes = CLONE_ATTRIBUTES.map do |attribute| + [attribute, build.send(attribute)] + end + + attributes.push([:user, current_user]) + + project.builds.create(Hash[attributes]) + end + end +end diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb new file mode 100644 index 00000000000..2c5e130e5aa --- /dev/null +++ b/app/services/ci/retry_pipeline_service.rb @@ -0,0 +1,22 @@ +module Ci + class RetryPipelineService < ::BaseService + def execute(pipeline) + unless can?(current_user, :update_pipeline, pipeline) + raise Gitlab::Access::AccessDeniedError + end + + pipeline.builds.failed_or_canceled.find_each do |build| + next unless build.retryable? + + Ci::RetryBuildService.new(project, current_user) + .reprocess(build) + end + + MergeRequests::AddTodoWhenBuildFailsService + .new(project, current_user) + .close_all(pipeline) + + pipeline.process! + end + end +end diff --git a/app/services/ci/update_runner_service.rb b/app/services/ci/update_runner_service.rb new file mode 100644 index 00000000000..450ee7da1c9 --- /dev/null +++ b/app/services/ci/update_runner_service.rb @@ -0,0 +1,15 @@ +module Ci + class UpdateRunnerService + attr_reader :runner + + def initialize(runner) + @runner = runner + end + + def update(params) + runner.update(params).tap do |updated| + runner.tick_runner_queue if updated + end + end + end +end diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb index 7f2d28086f5..2e2d7f884ac 100644 --- a/app/services/groups/destroy_service.rb +++ b/app/services/groups/destroy_service.rb @@ -8,7 +8,9 @@ module Groups end def execute - group.projects.each do |project| + group.prepare_for_destroy + + group.projects.with_deleted.each do |project| # Execute the destruction of the models immediately to ensure atomic cleanup. # Skip repository removal because we remove directory with namespace # that contain all these repositories diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb index 12a8415d9a5..727768b1a39 100644 --- a/app/services/merge_requests/add_todo_when_build_fails_service.rb +++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb @@ -18,5 +18,11 @@ module MergeRequests todo_service.merge_request_build_retried(merge_request) end end + + def close_all(pipeline) + pipeline_merge_requests(pipeline) do |merge_request| + todo_service.merge_request_build_retried(merge_request) + end + end end end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 5ca6fec962d..3da1b657a41 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -11,18 +11,20 @@ module MergeRequests def execute(merge_request) @merge_request = merge_request - return log_merge_error('Merge request is not mergeable', true) unless @merge_request.mergeable? + unless @merge_request.mergeable? + return log_merge_error('Merge request is not mergeable', save_message_on_model: true) + end @source = find_merge_source - return log_merge_error('No source for merge', true) unless @source + unless @source + return log_merge_error('No source for merge', save_message_on_model: true) + end merge_request.in_locked_state do if commit after_merge success - else - log_merge_error('Can not merge changes', true) end end end @@ -43,11 +45,11 @@ module MergeRequests if commit_id merge_request.update(merge_commit_sha: commit_id) else - merge_request.update(merge_error: 'Conflicts detected during merge') + log_merge_error('Conflicts detected during merge', save_message_on_model: true) false end rescue GitHooksService::PreReceiveError => e - merge_request.update(merge_error: e.message) + log_merge_error(e.message, save_message_on_model: true) false rescue StandardError => e merge_request.update(merge_error: "Something went wrong during merge: #{e.message}") @@ -70,10 +72,10 @@ module MergeRequests @merge_request.force_remove_source_branch? ? @merge_request.author : current_user end - def log_merge_error(message, http_error = false) + def log_merge_error(message, save_message_on_model: false) Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{message}") - error(message) if http_error + @merge_request.update(merge_error: message) if save_message_on_model end def merge_request_info diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 8ab943f4639..ad86b4f9f42 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -170,16 +170,20 @@ class TodoService # When user marks some todos as done def mark_todos_as_done(todos, current_user) - mark_todos_as_done_by_ids(todos.select(&:id), current_user) + update_todos_state_by_ids(todos.select(&:id), current_user, :done) end def mark_todos_as_done_by_ids(ids, current_user) - todos = current_user.todos.where(id: ids) + update_todos_state_by_ids(ids, current_user, :done) + end - # Only return those that are not really on that state - marked_todos = todos.where.not(state: :done).update_all(state: :done) - current_user.update_todos_count_cache - marked_todos + # When user marks some todos as pending + def mark_todos_as_pending(todos, current_user) + update_todos_state_by_ids(todos.select(&:id), current_user, :pending) + end + + def mark_todos_as_pending_by_ids(ids, current_user) + update_todos_state_by_ids(ids, current_user, :pending) end # When user marks an issue as todo @@ -194,6 +198,15 @@ class TodoService private + def update_todos_state_by_ids(ids, current_user, state) + todos = current_user.todos.where(id: ids) + + # Only return those that are not really on that state + marked_todos = todos.where.not(state: state).update_all(state: state) + current_user.update_todos_count_cache + marked_todos + end + def create_todos(users, attributes) Array(users).map do |user| next if pending_todos(user, attributes).exists? diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml index c4b748d0ab8..6c48328da4f 100644 --- a/app/views/admin/abuse_reports/index.html.haml +++ b/app/views/admin/abuse_reports/index.html.haml @@ -12,6 +12,7 @@ %th.wide Message %th Action = render @abuse_reports + = paginate @abuse_reports, theme: 'gitlab' - else .empty-state .text-center diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 816035ec442..749c74b8110 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -192,7 +192,7 @@ = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'control-label col-sm-2' .col-sm-10 = f.number_field :max_pages_size, class: 'form-control' - .help-block Zero for unlimited + .help-block 0 for unlimited %fieldset %legend Continuous Integration @@ -525,7 +525,7 @@ = f.number_field :terminal_max_session_time, class: 'form-control' .help-block Maximum time for web terminal websocket connection (in seconds). - Set to 0 for unlimited time. + 0 for unlimited. .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index 4f982a6e369..ac36bb5bb17 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -35,7 +35,7 @@ .clearfix %p %i.fa.fa-exclamation-circle - If '[25 of 25 busy]' is shown, restart GitLab with 'sudo service gitlab reload'. + If '[#{@concurrency} of #{@concurrency} busy]' is shown, restart GitLab with 'sudo service gitlab reload'. %p %i.fa.fa-exclamation-circle If more than one sidekiq process is listed, stop GitLab, kill the remaining sidekiq processes (sudo pkill -u #{gitlab_config.user} -f sidekiq) and restart GitLab. diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index 13d00dd1fcb..5e585ce789b 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -8,15 +8,14 @@ %div{ class: container_class } %ul.nav-links.log-tabs - loggers.each do |klass| - %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }> + %li{ class: active_when(klass == Gitlab::GitLogger) }> = link_to klass::file_name, "##{klass::file_name_noext}", 'data-toggle' => 'tab' .row-content-block To prevent performance issues admin logs output the last 2000 lines .tab-content - loggers.each do |klass| - .tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''), - id: klass::file_name_noext } + .tab-pane{ class: active_when(klass == Gitlab::GitLogger), id: klass::file_name_noext } .file-holder#README .js-file-title.file-title %i.fa.fa-file diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index cdef63daca9..c35945c5a35 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -48,13 +48,13 @@ = link_to admin_projects_path do All - = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do + = nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s) }) do = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do Private - = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do + = nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s) }) do = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do Internal - = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do + = nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s) }) do = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do Public diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 721bc77cc2f..d725e477044 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -56,7 +56,7 @@ = submit_tag 'Search', class: 'btn' .pull-right.light - Runners with last contact less than a minute ago: #{@active_runners_cnt} + Runners with last contact more than a minute ago: #{@active_runners_cnt} %br diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 4dc44225d49..298cf0fa950 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -38,31 +38,31 @@ .nav-block %ul.nav-links.wide.scrolling-tabs.white.scrolling-tabs .fade-left - = nav_link(html_options: { class: ('active' unless params[:filter]) }) do + = nav_link(html_options: { class: active_when(params[:filter].nil?) }) do = link_to admin_users_path do Active %small.badge= number_with_delimiter(User.active.count) - = nav_link(html_options: { class: ('active' if params[:filter] == 'admins') }) do + = nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do = link_to admin_users_path(filter: "admins") do Admins %small.badge= number_with_delimiter(User.admins.count) - = nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_enabled'} filter-two-factor-enabled" }) do + = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do = link_to admin_users_path(filter: 'two_factor_enabled') do 2FA Enabled %small.badge= number_with_delimiter(User.with_two_factor.count) - = nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_disabled'} filter-two-factor-disabled" }) do + = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do = link_to admin_users_path(filter: 'two_factor_disabled') do 2FA Disabled %small.badge= number_with_delimiter(User.without_two_factor.count) - = nav_link(html_options: { class: ('active' if params[:filter] == 'external') }) do + = nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do = link_to admin_users_path(filter: 'external') do External %small.badge= number_with_delimiter(User.external.count) - = nav_link(html_options: { class: ('active' if params[:filter] == 'blocked') }) do + = nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do = link_to admin_users_path(filter: "blocked") do Blocked %small.badge= number_with_delimiter(User.blocked.count) - = nav_link(html_options: { class: ('active' if params[:filter] == 'wop') }) do + = nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do = link_to admin_users_path(filter: "wop") do Without projects %small.badge= number_with_delimiter(User.without_projects.count) diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml index 68a46f61eb7..ecdf76ef5c5 100644 --- a/app/views/dashboard/_activity_head.html.haml +++ b/app/views/dashboard/_activity_head.html.haml @@ -1,8 +1,8 @@ .top-area %ul.nav-links - %li{ class: ("active" unless params[:filter]) }> + %li{ class: active_when(params[:filter].nil?) }> = link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do Your Projects - %li{ class: ("active" if params[:filter] == 'starred') }> + %li{ class: active_when(params[:filter] == 'starred') }> = link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do Starred Projects diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index 605bfd0cf8d..dc2d924f212 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -31,6 +31,9 @@ - if todo.pending? .todo-actions - = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do + = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading js-done-todo' do Done = icon('spinner spin') + = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-undo-todo hidden' do + Undo + = icon('spinner spin') diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index c4bf2c90cc2..16a5713948a 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -4,15 +4,13 @@ - if current_user.todos.any? .top-area %ul.nav-links - - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending') - %li{ class: "todos-pending #{todo_pending_active}" }> + %li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }> = link_to todos_filter_path(state: 'pending') do %span To do %span.badge = number_with_delimiter(todos_pending_count) - - todo_done_active = ('active' if params[:state] == 'done') - %li{ class: "todos-done #{todo_done_active}" }> + %li.todos-done{ class: active_when(params[:state] == 'done') }> = link_to todos_filter_path(state: 'done') do %span Done diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml index eddfce363a7..da4769e214e 100644 --- a/app/views/devise/shared/_signin_box.html.haml +++ b/app/views/devise/shared/_signin_box.html.haml @@ -4,7 +4,7 @@ .login-body = render 'devise/sessions/new_crowd' - @ldap_servers.each_with_index do |server, i| - .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: (:active if i.zero? && !crowd_enabled?) } + .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && !crowd_enabled?) } .login-body = render 'devise/sessions/new_ldap', server: server - if signin_enabled? diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml index 8c4ad30c832..dd34600490e 100644 --- a/app/views/devise/shared/_tabs_ldap.html.haml +++ b/app/views/devise/shared/_tabs_ldap.html.haml @@ -3,7 +3,7 @@ %li.active = link_to "Crowd", "#crowd", 'data-toggle' => 'tab' - @ldap_servers.each_with_index do |server, i| - %li{ class: (:active if i.zero? && !crowd_enabled?) } + %li{ class: active_when(i.zero? && !crowd_enabled?) } = link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab' - if signin_enabled? %li diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 64ca3c32e01..efd13aabf20 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -3,11 +3,9 @@ .event-title %span.author_name= link_to_author event %span.pushed #{event.action_name} #{event.ref_type} - - if event.rm_ref? - %strong= event.ref_name - - else - %strong - = link_to event.ref_name, namespace_project_commits_path(project.namespace, project, event.ref_name), title: h(event.target_title) + %strong + - commits_link = namespace_project_commits_path(project.namespace, project, event.ref_name) + = link_to_if project.repository.branch_exists?(event.ref_name), event.ref_name, commits_link = render "events/event_scope", event: event diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index e3088848492..56f463572bb 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -13,7 +13,7 @@ = link_to filter_projects_path(visibility_level: nil) do Any - Gitlab::VisibilityLevel.values.each do |level| - %li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' } + %li{ class: active_when(level.to_s == params[:visibility_level]) || 'light' } = link_to filter_projects_path(visibility_level: level) do = visibility_level_icon(level) = visibility_level_label(level) @@ -34,7 +34,7 @@ Any - @tags.each do |tag| - %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' } + %li{ class: active_when(tag.name == params[:tag]) || 'light' } = link_to filter_projects_path(tag: tag.name) do = icon('tag') = tag.name diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml index cefe0066a8f..5c5be03a7cd 100644 --- a/app/views/kaminari/gitlab/_page.html.haml +++ b/app/views/kaminari/gitlab/_page.html.haml @@ -6,5 +6,5 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li{ class: "page#{' active' if page.current?}#{' sibling' if page.next? || page.prev?}" } +%li.page{ class: [active_when(page.current?), ('sibling' if page.next? || page.prev?)] } = link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil } diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index f2d355587bd..302c1794628 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -23,7 +23,7 @@ %title= page_title(site_name) %meta{ name: "description", content: page_description } - = favicon_link_tag 'favicon.ico' + = favicon_link_tag favicon = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 54d02ee8e4b..a35a918d501 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,21 +1,4 @@ -.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } - .sidebar-wrapper.nicescroll - .sidebar-action-buttons - .nav-header-btn.toggle-nav-collapse{ title: "Open/Close" } - %span.sr-only Toggle navigation - = icon('bars') - - %div{ class: "nav-header-btn pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: { placement: 'right', container: 'body' } } - %span.sr-only Toggle navigation pinning - = icon('fw thumb-tack') - - - if defined?(sidebar) && sidebar - = render "layouts/nav/#{sidebar}" - - elsif current_user - = render 'layouts/nav/dashboard' - - else - = render 'layouts/nav/explore' - +.page-with-sidebar{ class: page_gutter_class } - if defined?(nav) && nav .layout-nav .container-fluid diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 248d439cd05..19bd9b6d5c9 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en", class: "#{page_class}" } = render "layouts/head" - %body{ class: "#{user_application_theme}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } + %body{ data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } = Gon::Base.render_data = render "layouts/header/default", title: header_title diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 59082ce5fd5..ddf50d6667f 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -2,9 +2,15 @@ %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content .container-fluid .header-content - %button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" } - %span.sr-only Toggle navigation - = icon('bars') + .dropdown.global-dropdown + %button.global-dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } + %span.sr-only Toggle navigation + = icon('bars') + .dropdown-menu-nav.global-dropdown-menu + - if current_user + = render 'layouts/nav/dashboard' + - else + = render 'layouts/nav/explore' %button.navbar-toggle{ type: 'button' } %span.sr-only Toggle navigation = icon('ellipsis-v') @@ -55,7 +61,6 @@ %div = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success' - %h1.title= title .header-logo diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 205d23178d2..5d4178f03d7 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,41 +1,39 @@ -.nav-sidebar - %ul.nav - = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do - = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do - %span - Projects - = nav_link(path: 'dashboard#activity') do - = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do - %span - Activity - - if koding_enabled? - = nav_link(controller: :koding) do - = link_to koding_path, title: 'Koding' do - %span - Koding - = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do - = link_to dashboard_groups_path, title: 'Groups' do - %span - Groups - = nav_link(controller: 'dashboard/milestones') do - = link_to dashboard_milestones_path, title: 'Milestones' do - %span - Milestones - = nav_link(path: 'dashboard#issues') do - = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do - %span - Issues - %span.count= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened)) - = nav_link(path: 'dashboard#merge_requests') do - = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do - %span - Merge Requests - %span.count= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened)) - = nav_link(controller: 'dashboard/snippets') do - = link_to dashboard_snippets_path, title: 'Snippets' do +%ul + = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do + = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do + %span + Projects + = nav_link(path: 'dashboard#activity') do + = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do + %span + Activity + - if koding_enabled? + = nav_link(controller: :koding) do + = link_to koding_path, title: 'Koding' do %span - Snippets - - = link_to help_path, title: 'About GitLab CE', class: 'about-gitlab' do + Koding + = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do + = link_to dashboard_groups_path, title: 'Groups' do + %span + Groups + = nav_link(controller: 'dashboard/milestones') do + = link_to dashboard_milestones_path, title: 'Milestones' do + %span + Milestones + = nav_link(path: 'dashboard#issues') do + = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do + %span + Issues + (#{number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))}) + = nav_link(path: 'dashboard#merge_requests') do + = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do + %span + Merge Requests + (#{number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))}) + = nav_link(controller: 'dashboard/snippets') do + = link_to dashboard_snippets_path, title: 'Snippets' do %span - About GitLab CE + Snippets + %li.divider + %li + = link_to "About GitLab CE", help_path, title: 'About GitLab CE', class: 'about-gitlab' diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml index e5bda7b3a6f..3a1fcd00e9c 100644 --- a/app/views/layouts/nav/_explore.html.haml +++ b/app/views/layouts/nav/_explore.html.haml @@ -1,4 +1,4 @@ -%ul.nav.nav-sidebar +%ul = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do = link_to explore_root_path, title: 'Projects' do %span diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index feadd863b00..df0a0212f3d 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -4,19 +4,6 @@ = form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f| .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 - Application theme - %p - This setting allows you to customize the appearance of the site, e.g. the sidebar. - .col-lg-9.application-theme - - Gitlab::Themes.each do |theme| - = label_tag do - .preview{ class: theme.css_class } - = f.radio_button :theme_id, theme.id - = theme.name - .col-sm-12 - %hr - .col-lg-3.profile-settings-sidebar - %h4.prepend-top-0 Syntax highlighting theme %p This setting allow you to customize the appearance of the syntax. diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb index 8966dd3fd86..431ab9d052b 100644 --- a/app/views/profiles/preferences/update.js.erb +++ b/app/views/profiles/preferences/update.js.erb @@ -1,7 +1,3 @@ -// Remove body class for any previous theme, re-add current one -$('body').removeClass('<%= Gitlab::Themes.body_classes %>') -$('body').addClass('<%= user_application_theme %>') - // Toggle container-fluid class if ('<%= current_user.layout %>' === 'fluid') { $('.content-wrapper .container-fluid').removeClass('container-limited') diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index 3b1a2e54ec2..d1f7f65bf53 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -25,6 +25,6 @@ = link_to raw(line_new), "##{line_new}" = line_content - - if @form.unfold? && @form.bottom? && @form.to < @blob.loc + - if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size %tr.line_holder{ id: @form.to, class: line_class } = diff_match_line @form.to - @form.offset, @form.to, text: @match_line, view: diff_view, bottom: true diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 762ff34a9ec..b560ed21f1d 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -8,19 +8,19 @@ %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } %li.dropdown-header Source code %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow', download: '' do %i.fa.fa-download %span Download zip %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow', download: '' do %i.fa.fa-download %span Download tar.gz %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow', download: '' do %i.fa.fa-download %span Download tar.bz2 %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow', download: '' do %i.fa.fa-download %span Download tar @@ -36,6 +36,6 @@ %li.dropdown-header Previous Artifacts - artifacts.each do |job| %li - = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do + = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow', download: '' do %i.fa.fa-download %span Download '#{job.name}' diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index f852f2e3fd7..3475fa5f960 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -78,7 +78,7 @@ %ul.dropdown-menu.dropdown-menu-align-right - artifacts.each do |build| %li - = link_to download_namespace_project_build_artifacts_path(pipeline.project.namespace, pipeline.project, build), rel: 'nofollow' do + = link_to download_namespace_project_build_artifacts_path(pipeline.project.namespace, pipeline.project, build), rel: 'nofollow', download: '' do = icon("download") %span Download '#{build.name}' artifacts diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml index b87b79b170e..5c38b5ad9c0 100644 --- a/app/views/projects/diffs/_content.html.haml +++ b/app/views/projects/diffs/_content.html.haml @@ -15,10 +15,13 @@ %a.click-to-expand Click to expand it. - elsif diff_file.diff_lines.length > 0 + - total_lines = 0 + - if blob.lines.any? + - total_lines = blob.lines.last.chomp == '' ? blob.lines.size - 1 : blob.lines.size - if diff_view == :parallel - = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob + = render "projects/diffs/parallel_view", diff_file: diff_file, total_lines: total_lines - else - = render "projects/diffs/text_file", diff_file: diff_file + = render "projects/diffs/text_file", diff_file: diff_file, total_lines: total_lines - else - if diff_file.mode_changed? .nothing-here-block File mode changed diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 074f1f634ae..997bf0fc560 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -43,7 +43,8 @@ - discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file) - if discussion_left || discussion_right = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right - - if !diff_file.new_file && diff_file.diff_lines.any? + - if !diff_file.new_file && !diff_file.deleted_file && diff_file.diff_lines.any? - last_line = diff_file.diff_lines.last - %tr.line_holder.parallel - = diff_match_line last_line.old_pos, last_line.new_pos, bottom: true, view: :parallel + - if last_line.new_pos < total_lines + %tr.line_holder.parallel + = diff_match_line last_line.old_pos, last_line.new_pos, bottom: true, view: :parallel diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 2eea1db169a..ebd1a914ee7 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -10,7 +10,8 @@ as: :line, locals: { diff_file: diff_file, discussions: discussions } - - if !diff_file.new_file && diff_file.highlighted_diff_lines.any? + - if !diff_file.new_file && !diff_file.deleted_file && diff_file.highlighted_diff_lines.any? - last_line = diff_file.highlighted_diff_lines.last - %tr.line_holder - = diff_match_line last_line.old_pos, last_line.new_pos, bottom: true + - if last_line.new_pos < total_lines + %tr.line_holder + = diff_match_line last_line.old_pos, last_line.new_pos, bottom: true diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml new file mode 100644 index 00000000000..d9cb7bc0331 --- /dev/null +++ b/app/views/projects/environments/folder.html.haml @@ -0,0 +1,13 @@ +- @no_container = true +- page_title "Environments" += render "projects/pipelines/head" + +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag("environments_folder") + +#environments-folder-list-view{ data: { "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, + "can-read-environment" => can?(current_user, :read_environment, @project).to_s, + "css-class" => container_class, + "commit-icon-svg" => custom_icon("icon_commit"), + "terminal-icon-svg" => custom_icon("icon_terminal"), + "play-icon-svg" => custom_icon("icon_play") } } diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index e3062f47788..c676953f729 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -28,7 +28,7 @@ %span CI job = ci_label_for_status(status) - for + for - commit = @merge_request.diff_head_commit = succeed "." do = link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace" diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 1b08165c14c..a73e8f345e0 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -71,7 +71,7 @@ - if note_editable .original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } } #{note.note} - %textarea.hidden.js-task-list-field.original-task-list= note.note + %textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: namespace_project_note_path(@project.namespace, @project, note) } }= note.note .note-awards = render 'award_emoji/awards_block', awardable: note, inline: false - if note.system diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index a6cd2d83bd5..e0c972aa2fb 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -7,9 +7,9 @@ = commit_author_link(@commit) .header-action-buttons - if can?(current_user, :update_pipeline, @pipeline.project) - - if @pipeline.builds.latest.failed.any?(&:retryable?) + - if @pipeline.retryable? = link_to "Retry failed", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'btn btn-inverted-secondary', method: :post - - if @pipeline.builds.running_or_pending.any? + - if @pipeline.cancelable? = link_to "Cancel running", cancel_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - if @commit diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 81e393d7626..6e0428e2a31 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -5,23 +5,23 @@ %div{ class: container_class } .top-area %ul.nav-links - %li{ class: ('active' if @scope.nil?) }> + %li{ class: active_when(@scope.nil?) }> = link_to project_pipelines_path(@project) do All %span.badge.js-totalbuilds-count = number_with_delimiter(@pipelines_count) - %li{ class: ('active' if @scope == 'running') }> + %li{ class: active_when(@scope == 'running') }> = link_to project_pipelines_path(@project, scope: :running) do Running %span.badge.js-running-count = number_with_delimiter(@running_or_pending_count) - %li{ class: ('active' if @scope == 'branches') }> + %li{ class: active_when(@scope == 'branches') }> = link_to project_pipelines_path(@project, scope: :branches) do Branches - %li{ class: ('active' if @scope == 'tags') }> + %li{ class: active_when(@scope == 'tags') }> = link_to project_pipelines_path(@project, scope: :tags) do Tags diff --git a/app/views/projects/wikis/_sidebar_wiki_page.html.haml b/app/views/projects/wikis/_sidebar_wiki_page.html.haml index eb9bd14920d..0a61d90177b 100644 --- a/app/views/projects/wikis/_sidebar_wiki_page.html.haml +++ b/app/views/projects/wikis/_sidebar_wiki_page.html.haml @@ -1,3 +1,3 @@ -%li{ class: params[:id] == wiki_page.slug ? 'active' : '' } +%li{ class: active_when(params[:id] == wiki_page.slug) } = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do = wiki_page.title.capitalize diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index 8cbecb725b5..5afb95ac430 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -1,70 +1,70 @@ %ul.nav-links.search-filter - if @project - %li{ class: ("active" if @scope == 'blobs') } + %li{ class: active_when(@scope == 'blobs') } = link_to search_filter_path(scope: 'blobs') do Code %span.badge = @search_results.blobs_count - %li{ class: ("active" if @scope == 'issues') } + %li{ class: active_when(@scope == 'issues') } = link_to search_filter_path(scope: 'issues') do Issues %span.badge = @search_results.issues_count - %li{ class: ("active" if @scope == 'merge_requests') } + %li{ class: active_when(@scope == 'merge_requests') } = link_to search_filter_path(scope: 'merge_requests') do Merge requests %span.badge = @search_results.merge_requests_count - %li{ class: ("active" if @scope == 'milestones') } + %li{ class: active_when(@scope == 'milestones') } = link_to search_filter_path(scope: 'milestones') do Milestones %span.badge = @search_results.milestones_count - %li{ class: ("active" if @scope == 'notes') } + %li{ class: active_when(@scope == 'notes') } = link_to search_filter_path(scope: 'notes') do Comments %span.badge = @search_results.notes_count - %li{ class: ("active" if @scope == 'wiki_blobs') } + %li{ class: active_when(@scope == 'wiki_blobs') } = link_to search_filter_path(scope: 'wiki_blobs') do Wiki %span.badge = @search_results.wiki_blobs_count - %li{ class: ("active" if @scope == 'commits') } + %li{ class: active_when(@scope == 'commits') } = link_to search_filter_path(scope: 'commits') do Commits %span.badge = @search_results.commits_count - elsif @show_snippets - %li{ class: ("active" if @scope == 'snippet_blobs') } + %li{ class: active_when(@scope == 'snippet_blobs') } = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do Snippet Contents %span.badge = @search_results.snippet_blobs_count - %li{ class: ("active" if @scope == 'snippet_titles') } + %li{ class: active_when(@scope == 'snippet_titles') } = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do Titles and Filenames %span.badge = @search_results.snippet_titles_count - else - %li{ class: ("active" if @scope == 'projects') } + %li{ class: active_when(@scope == 'projects') } = link_to search_filter_path(scope: 'projects') do Projects %span.badge = @search_results.projects_count - %li{ class: ("active" if @scope == 'issues') } + %li{ class: active_when(@scope == 'issues') } = link_to search_filter_path(scope: 'issues') do Issues %span.badge = @search_results.issues_count - %li{ class: ("active" if @scope == 'merge_requests') } + %li{ class: active_when(@scope == 'merge_requests') } = link_to search_filter_path(scope: 'merge_requests') do Merge requests %span.badge = @search_results.merge_requests_count - %li{ class: ("active" if @scope == 'milestones') } + %li{ class: active_when(@scope == 'milestones') } = link_to search_filter_path(scope: 'milestones') do Milestones %span.badge diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index f7808ea6aff..e977c1f1698 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -7,7 +7,7 @@ = snippet.title by = link_to user_snippets_path(snippet.author) do - = image_tag avatar_icon(snippet.author_email), class: "avatar avatar-inline s16", alt: '' + = image_tag avatar_icon(snippet.author), class: "avatar avatar-inline s16", alt: '' = snippet.author_name %span.light= time_ago_with_tooltip(snippet.created_at) %h4.snippet-title diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index 704d1d01a81..026f404ce07 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -18,6 +18,6 @@ %span by = link_to user_snippets_path(snippet_title.author) do - = image_tag avatar_icon(snippet_title.author_email), class: "avatar avatar-inline s16", alt: '' + = image_tag avatar_icon(snippet_title.author), class: "avatar avatar-inline s16", alt: '' = snippet_title.author_name %span.light= time_ago_with_tooltip(snippet_title.created_at) diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index ead9b84b991..1744a597c51 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -1,6 +1,4 @@ - label_css_id = dom_id(label) -- open_issues_count = label.open_issues_count(current_user) -- open_merge_requests_count = label.open_merge_requests_count(current_user) - status = label_subscription_status(label, @project).inquiry if current_user - subject = local_assigns[:subject] @@ -15,10 +13,10 @@ %ul %li = link_to_label(label, subject: subject, type: :merge_request) do - = pluralize open_merge_requests_count, 'merge request' + view merge requests %li = link_to_label(label, subject: subject) do - = pluralize open_issues_count, 'open issue' + view open issues - if current_user && defined?(@project) %li.label-subscription - if label.is_a?(ProjectLabel) @@ -40,9 +38,9 @@ .pull-right.hidden-xs.hidden-sm.hidden-md = link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action') do - = pluralize open_merge_requests_count, 'merge request' + view merge requests = link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action') do - = pluralize open_issues_count, 'open issue' + view open issues - if current_user && defined?(@project) .label-subscription.inline diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml index b6047ece592..3baa956b910 100644 --- a/app/views/shared/builds/_tabs.html.haml +++ b/app/views/shared/builds/_tabs.html.haml @@ -1,23 +1,23 @@ %ul.nav-links - %li{ class: ('active' if scope.nil?) }> + %li{ class: active_when(scope.nil?) }> = link_to build_path_proc.call(nil) do All %span.badge.js-totalbuilds-count = number_with_delimiter(all_builds.count(:id)) - %li{ class: ('active' if scope == 'pending') }> + %li{ class: active_when(scope == 'pending') }> = link_to build_path_proc.call('pending') do Pending %span.badge = number_with_delimiter(all_builds.pending.count(:id)) - %li{ class: ('active' if scope == 'running') }> + %li{ class: active_when(scope == 'running') }> = link_to build_path_proc.call('running') do Running %span.badge = number_with_delimiter(all_builds.running.count(:id)) - %li{ class: ('active' if scope == 'finished') }> + %li{ class: active_when(scope == 'finished') }> = link_to build_path_proc.call('finished') do Finished %span.badge diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index 1154316c03f..ad995cbe962 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -3,23 +3,23 @@ - issuables = @issues || @merge_requests %ul.nav-links.issues-state-filters - %li{ class: ("active" if params[:state] == 'opened') }> + %li{ class: active_when(params[:state] == 'opened') }> = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened." do #{issuables_state_counter_text(type, :opened)} - if type == :merge_requests - %li{ class: ("active" if params[:state] == 'merged') }> + %li{ class: active_when(params[:state] == 'merged') }> = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.' do #{issuables_state_counter_text(type, :merged)} - %li{ class: ("active" if params[:state] == '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.' do #{issuables_state_counter_text(type, :closed)} - else - %li{ class: ("active" if params[:state] == 'closed') }> + %li{ class: active_when(params[:state] == 'closed') }> = link_to page_filter_path(state: 'closed', label: true), id: 'state-all', title: 'Filter by issues that are currently closed.' do #{issuables_state_counter_text(type, :closed)} - %li{ class: ("active" if params[:state] == 'all') }> + %li{ class: active_when(params[:state] == 'all') }> = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}." 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 31eb07ca666..a93cbd1041f 100644 --- a/app/views/shared/milestones/_issuables.html.haml +++ b/app/views/shared/milestones/_issuables.html.haml @@ -3,11 +3,11 @@ - panel_class = primary ? 'panel-primary' : 'panel-default' .panel{ class: panel_class } - .panel-heading.split - .left + .panel-heading + .title = title - if show_counter - .right + .counter = number_with_delimiter(issuables.size) - class_prefix = dom_class(issuables).pluralize diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml index 2dda5fed647..8b6a98a054a 100644 --- a/app/views/snippets/_snippets_scope_menu.html.haml +++ b/app/views/snippets/_snippets_scope_menu.html.haml @@ -2,7 +2,7 @@ - include_private = local_assigns.fetch(:include_private, false) .nav-links.snippet-scope-menu - %li{ class: ("active" unless params[:scope]) } + %li{ class: active_when(params[:scope].nil?) } = link_to subject_snippets_path(subject) do All %span.badge @@ -12,19 +12,19 @@ = subject.snippets.public_and_internal.count - if include_private - %li{ class: ("active" if params[:scope] == "are_private") } + %li{ class: active_when(params[:scope] == "are_private") } = link_to subject_snippets_path(subject, scope: 'are_private') do Private %span.badge = subject.snippets.are_private.count - %li{ class: ("active" if params[:scope] == "are_internal") } + %li{ class: active_when(params[:scope] == "are_internal") } = link_to subject_snippets_path(subject, scope: 'are_internal') do Internal %span.badge = subject.snippets.are_internal.count - %li{ class: ("active" if params[:scope] == "are_public") } + %li{ class: active_when(params[:scope] == "are_public") } = link_to subject_snippets_path(subject, scope: 'are_public') do Public %span.badge |