diff --git a/.gitlab/issue_templates/Research Proposal.md b/.gitlab/issue_templates/Research Proposal.md new file mode 100644 index 0000000000000000000000000000000000000000..5676656793dab1ebc18e7d688100c3c5ec82be82 --- /dev/null +++ b/.gitlab/issue_templates/Research Proposal.md @@ -0,0 +1,17 @@ +### Background: + +(Include problem, use cases, benefits, and/or goals) + +**What questions are you trying to answer?** + +**Are you looking to verify an existing hypothesis or uncover new issues you should be exploring?** + +**What is the backstory of this project and how does it impact the approach?** + +**What do you already know about the areas you are exploring?** + +**What does success look like at the end of the project?** + +### Links / references: + +/label ~"UX research" diff --git a/CHANGELOG.md b/CHANGELOG.md index cbaac0f69d3a1c7c2f0d500ea465cbd6c58c4da5..58b8cf2ad831991ab2ea4c8f01e723e63635c782 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.16.6 (2017-02-17) + +- API: Fix file downloading. !0 (8267) +- Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms. !8752 +- Fix filtered search user autocomplete for gitlab instances that are hosted on a subdirectory. !8891 +- Fix wrong call to ProjectCacheWorker.perform. !8910 +- Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index. !8956 +- Fix broken anchor links when special characters are used. !8961 (Andrey Krivko) +- Do not display deploy keys in user's own ssh keys list. !9024 +- Show merge errors in merge request widget. !9229 +- Don't delete assigned MRs/issues when user is deleted. +- backport of EE fix !954. +- Refresh authorizations when transferring projects. +- Don't use backup Active Record connections for Sidekiq. +- Check public snippets for spam. + ## 8.16.5 (2017-02-14) - Patch Asciidocs rendering to block XSS. @@ -181,6 +197,10 @@ entry. - Add margin to markdown math blocks. - Add hover state to MR comment reply button. +## 8.15.7 (2017-02-15) + +- No changes. + ## 8.15.6 (2017-02-14) - Patch Asciidocs rendering to block XSS. @@ -451,6 +471,10 @@ entry. - Whitelist next project names: help, ci, admin, search. !8227 - Adds back CSS for progress-bars. !8237 +## 8.14.10 (2017-02-15) + +- No changes. + ## 8.14.9 (2017-02-14) - Patch Asciidocs rendering to block XSS. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 72cd57ad7fff8c364c7931b5e3e1b8ed548bae1d..de32a953f631dbf95fdd555a47a1b3e3cb819388 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,18 +93,20 @@ Please see the [UX Guide for GitLab]. ### Retrospective -After each release (usually on the 22nd of each month), we have a retrospective -call where we discuss what went well, what went wrong, and what we can improve -for the next release. The [retrospective notes] are public and you are invited -to comment them. -If you're interested, you can even join the [retrospective call][retro-kickoff-call]. +After each release, we have a retrospective call where we discuss what went well, +what went wrong, and what we can improve for the next release. The +[retrospective notes] are public and you are invited to comment on them. +If you're interested, you can even join the +[retrospective call][retro-kickoff-call], on the first working day after the +22nd at 6pm CET / 9am PST. ### Kickoff -Before working on the next release (usually on the 8th of each month), we have a +Before working on the next release, we have a kickoff call to explain what we expect to ship in the next release. The -[kickoff notes] are public and you are invited to comment them. -If you're interested, you can even join the [kickoff call][retro-kickoff-call]. +[kickoff notes] are public and you are invited to comment on them. +If you're interested, you can even join the [kickoff call][retro-kickoff-call], +on the first working day after the 7th at 6pm CET / 9am PST.. [retrospective notes]: https://docs.google.com/document/d/1nEkM_7Dj4bT21GJy0Ut3By76FZqCfLBmFQNVThmW2TY/edit?usp=sharing [kickoff notes]: https://docs.google.com/document/d/1ElPkZ90A8ey_iOkTvUs_ByMlwKK6NAB2VOK5835wYK0/edit?usp=sharing diff --git a/app/assets/images/favicon-blue.ico b/app/assets/images/favicon-blue.ico new file mode 100755 index 0000000000000000000000000000000000000000..156fcf075881c121ea364095cef181e5cc7fd285 Binary files /dev/null and b/app/assets/images/favicon-blue.ico differ diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index 424dc719c78f7bae705af7747b5f19522e6de43d..aaed74d6073d6df857c88dfa47618e9d7ead0240 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 84bbe90f3b10b2e455c571e9cf5c438d0269966f..86e0ad894319e64ccc8922bfd5702fd64cd39d75 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 4b5c9686cab09d6197107aa753ed9771513b8e39..8e468faedbf4f44ddd54345e984c3e2bc8dc9a16 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 8438de6cdf14f0029f13299e5e655bb32e7d2be9..448e6e2cc78764a72ce16b7d4e4998df51fcd781 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 b16a2c0f73aba2183e1123e288b787a0c21271d5..e55405135fb9de1951942000a2b045f808628916 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 9d776b749651f88d3b56c340bd7a87b1c84e8d06..a4ccb30e447aa220faae5529ef0e1b13cada0974 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 a489523b802c759000efa2fa4d78e66080b7b489..f7f41d55b52bf9a3d8952b5c077d9bf401b27d17 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 6af8f59387210879a3d81771a9e215ee2827fd90..fd0840fa1172a983ed50196dd8a2d70ae1df26e4 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 7747306688cfa111ff8f7713d73f29b467d96396..a7e68ae5cb96fc1f3047b770ae9e57a43f9d3992 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 6276933e93e9f499f8eb48622cf6eb4ab806b771..6b21695d082f40ee577f8f144ba381321e0b0ee5 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 04bfe3639297bf5a62b74980cb9a500da97c80bc..5f14ff40eee817186e1605cac3d9e9ba2b5f5f32 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 1d0bcf6471f521d37fab480106a198320991c7eb..de20eab9cd1c74b37d7a671499bd14e3ea7bc2ad 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 8236457f0f1cd4c69c651b02e28f63025279e5a6..43e5c0a56419ff9bf1019b970b8724fa3274f755 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 1d5672d4c48f0acc9de949062cc5699e9056632f..b582052a76e5ddf74f8567b2bd561f734dfe9d6f 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 9e0754819fae797e01ece96ba03cd5c1b0d5e438..0436bbb0eaf65988364a62ac64d124bebdc81c4b 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 079445e8278213861623e31e149bd0b89806bbe7..a1127b9e30e7460fe89d2475cfb829eb9c172f92 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 8f30900198e43e74dce4b942f03673733cb94804..878ad1b6031eb469cc68fa9a67fcdb9e7b1f013c 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 f8dac1ff56e71657f039220a46cb730b2dc5194a..22e9332854840137631b1d872cb2b4b1f3d5aec0 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 dbdadc73c3fc5fcbd7865276f5dd602738cdb9f2..e8531c43b4b4883723478ed82338fd6e40a21596 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 c5a962dd1994cc3f18697fc1378fff23db33e851..8fa1aceddffa725e135224d0eed0be2b736ad72e 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 083448552b6a436e9b0020e4d7841f9698fd9804..cae9a0ffca46f25aa421b5be359c4f514f6702ac 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 c656ae4e241867fcd8a6ca0bd52a2c49a61d4fb3..566b322eb4906c947376c74d1a33c72045c63fcc 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 184b4561d2edff3f34e081e21d1c24778225e6de..ee087c978dd836dcfdfdcd707ad850a38d806c9f 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 f09a6b1e6769e13d11069c215e65ec106f57ddbc..49bb64a3472e2baabb5efcea6816be60b03a6c1d 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 f1b41911b73d21a217d6aeb56366e8b8c89f15ec..f1b80e45444568616f02b8c805dd3d1b3e7b11e0 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 5c1a7eb10522650d3ff1521dbdafe7e83086360a..e7c6c063413893ac74bc132bd3c152abb60d9d0a 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 c6fdfbcaa105ac889bed3f7a1cfb777b76078b03..ccd895f3bf4e7cb8377096586228565ab122aef8 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 9591df70e9c762c8db28429969ef48e422f5268f..15df105d4ccb308038a79b3c8683c3879e75ae8e 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 3587431ab69d9c01e3274aa0adcf8aa251ab4a2c..1eca973e06907d6faf3c4e9bb739c1befc0d090f 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 35d98492012ed7605ecb23235569fd7d6ae61700..a1c1b721228b353e7e5bf89197a8270946b0e6d9 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 0029c59e550890f501b9de57dd7075820c896170..615f485e18acbab12543672bd92e4f31368f5d17 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 dbdb01c8c68a0a801835d3823e1df1aaffb9a87c..1ac715aab7706429f4db7d7bdddcac5dc12249a2 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 be732971c7f1467d7753750c791cf40093f62a50..3efeb14100854e1cfb667eb16a0610a10a3177ff 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 0000000000000000000000000000000000000000..cfaf9835bf8f5a148839d827c0b5bbc94ae0d1e0 --- /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 7eec2d39a9c7fb04f57b41f202029039e054aab2..45aa6050aed3453d5a2c7fd1fd8b558e823cd5e8 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 a510eebae1af060f444108e0b9b018c811175813..64a7a9eaf3736ef01ab9646d907793326511097d 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 91553bda4dc58d2b6a8d4a65ca0fae6d775c4bda..4b700a39d444bdde28b6c7f7beaeecb1fd9020fe 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 ed1c78945db8515e9181391900c8651ef910b6fa..c5a714d967337f5de4361cc198a171736a21e30c 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 28cc0022d178f2285e97579f033e211312fd19a7..2599bba3c59211600648038da82a36988e310091 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 39746621c43319f3697e2da6a15c4193617307f0..24fd58a301ae7c610efa6ed2dc90562627c652b1 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 5938340a128413b19fa8b39276512dbdda5fb5c8..daf126eb4e81851033cb734243eee614af6f2c84 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 be9526989a010dbbbf91ae9d547ca14c12591a01..96983a19568aaf943a537eab4f9530a0ec1e35a7 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 a3ad063f7cb1bf4f3814bb09cc2b02ae077eb81a..481e0d15e7a9ea58407a759d66291a950d729545 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 0000000000000000000000000000000000000000..fd35d77fd3d913b4080fcbbb232de3d67a761763 --- /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 05c59d92fd4fe29e892912451a66899eaf19efa0..7bbba91bc109bce83de80b7430c6bbb3aa81b509 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 0000000000000000000000000000000000000000..d2ca465351a6887e82e0da5c82250b93cbcb1595 --- /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 0000000000000000000000000000000000000000..53d5296575869aff7878226a315e8c9116a73a65 --- /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 fab8d977f5852ec4da8af262115d98ba4e956ef7..9cef335868ecc5f75b047997ae0cfa08a7b55f94 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 9b4090100da10016fea8abdeff257dcd814e2ae0..15cd9bde08e8b4e7f4908f4eee51a512923b6a7f 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 d3b58b2707a4e1e65d2302bd135584e1d8d99a04..1a489b859e80b25dcb56eed4eb4fb1ef0b574047 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 895a872568dff78e99e2b1a3dd466031608a3c6e..698870d0ce1affae29b11b86a19f44fa3b41d271 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 249fe23d4cb6c9da21bd721e4bad7f68c716a7ca..730104b89f9a5661fa68a9100bc7092b8a73ae26 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 7f1f2a5d278c3ac7209df0c592be81f05895203a..60d6658dc16687e9e3d21aa18862f2d65e86ba81 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 0d618caf35092c93991f0233b6b57d4a4a46ef3b..bf3da8528f004c8a80abf5557a44bcf86992d7b1 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 2e6da5750de5291d4a8e1f6b4c1f9a89362a7c64..75a53aae33cac91ca53454fd750320e2b59f4434 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 d06a1a5dae4965505d38e2997d0c02b9aa714cb9..bbfb467ad5079a722edd65135e58a71d34c74ec6 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 241249fae63840924b6fe56026e78014af2243ee..228771da4eed5596b0d2020c50f46d0535bb1938 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 29c3163328fc1ddfb0dae301c76716d0d3e1aeaa..7954c583598fc55ed16562732e2f812fadd3c64c 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 10dfd05fe3c4e85ba455e279d48fc9fe3d7cd085..c5cb273c5b28e75e99813dec45da5c836e5351dd 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 bc88dc2d092f986d6e75bb392f460177973fce32..6b937e7fa0f1047594bc6e4bcd85d2c108fa3d7f 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 9390136d3d82849ec205721ca6b6ae45e6988604..34e4a257ff948b10eb24be8158d6c7f073e4e1ce 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 c77fbb6a1c7834783741d73a07043de45f804045..115312d4b838835400f880259c5257c00da4c249 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 c7c744ef61f9608f52de6c0dbe2dd0f571920de6..de184ab26754ce4a711112506b85dc2184275420 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 6c08b1b8e6170110f8a024e728d0ba099a383588..52457f70d908c447bb789009b936f6283ce9fc1e 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 1d6eff1140370eca1f9aeb4b48bb5c209d13e891..b2cfd3ef2a3cba652c32e525e731173ee570658c 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 40ad6fc348efb554c1aaf641de07a57d1cac7b80..17a3fc1b1e48bf6fb11d16a30a42c1d781b7b211 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 e4cf9057e6da7113201d1a7cfd80bd17bff5ca33..9e2d14c7f8776995e19b718ed8333ca7ee90f181 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 1c0ea317c1aa7895ab5ae813f121d9e4bd78672b..08ca9e4fa4dd14a9d284f53f4ee348df7ff9bb64 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 5221f85ba7a4aff0c4b11e0f1c3d5fd2ac6e8d4c..7862c6797c3366a33ddebda89a491de4f8d34740 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 5a9a501efe3bc9fb31288e570034f62ee3a0e8b6..ebe1e2ae98dc92bfd442683f6597f4c26b93788d 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 ce090a2e4fdacd6053fb62a39d6b6ce9f8b62c4f..d93c1d0da59a8f447a5a6a38f8ba8cad11355eef 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 bcb3a706b5122ed618e9af1b0499c03d5d5820f5..45a1d90a9d997107780f3d77b97744fee3f976d3 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -231,6 +231,21 @@ return upperCaseHeaders; }; + /** + * 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. * @@ -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 f41fa15b147dfa881c82b0f551684d9fd21006f4..82dcbdc26c8cf2080416a697ac7bddfe73b6f8f5 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 6d5979603b91323e077d3bc83c1166a3d20789c5..66f39122a663f4de230084e87fb5451f4dc6dfa7 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 d9370db0cf2ff769a0cf9c4fc883dca9a9cd8f9c..f755d212b3cb8d8495f9fc99ce76fcc842966480 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 6d813d616016aad59b35ba7ef9390a00b4060403..db62e0be324d39e726baf7c39a7e8a2de50ba36c 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 a1558b371f09da478aa205b25ada6283a5a02539..1bc81d2e4a4e320074cd0ed1e49ed7d64794262c 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 d7137ec63e416d06dbdf9973c52a0c211a92d564..966fcd8ec4774ae39e5c992e4aee028fe24f4f1c 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 1b0d0768db8de4346b7eebd91441ecd9740f4bbc..729baa2e1a70f7be2d58a4850cba5542f1c39ab0 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 efe7c78a8ec8cd5f5aaeaf4b56bb8c7a712034b9..129d2dc5f0a8d695949f738059175316f422c60c 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 e65378cd610cf8d56e166ffb0003b624bb92d185..5e01aacf2ba4a44b77071e8e52a417c6c6c215b3 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 4ab33420e592b2c4bb57a49e52bf94511af00973..88f08bbaa345ee6cafdf524a569a64308b31a804 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -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 527cdc9b6986f8cc9d29af429406f7e88a87cfdd..9548a98f499ec87f0777a3e1961e168102540d9b 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 051cb9fe5c5991a3b23e0b10db79edbe8d18b74f..7fbaeec7882c0d4922256924b272b5dd1ee80f9e 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 2f08aa7fe8bac5ddaa78e98f6351b060ea4f4331..8df1c8e7f9463ca016c3368cfc90e71db5956694 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 919fcd0a07bab4f90a0f9ad663b98fe2f2657df9..2145e531331daa00c2405a68f8cda7ef355f4079 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 2ae5617206eded471fe746f89cd255daaa3564c0..b98e6121967a7336281f840ca7cf7b4f6d846882 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 a7ccd03b60c6e097f86fa75d90f871e18ea81b91..43dc9838977772245f1b93a6b1127868cf419b66 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 37bf6436fd147b9ed1d6765cb236882c2047c6b5..8e7027b44e7c996e9bfe3308e6a5f672c1b956f5 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 b4491354472a11f9f97aab36ab49c5b75a3d3776..aae509caa799e4fd7f1f3b005a16ad9f76117124 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 7f763c13b507327ed5e42e96de8e03c32b67631d..cb24f212c66e818328bcbad05397f3b8943af9e5 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 41eea78a3e648dc417e87febb82c27255fd4dae4..747f693726e131a252c3786da65b4571bbe68c12 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 0464b895d6dbdd37026098c0e0630ad42b3c5693..03504255bdae3632cf0fee62e41a23d41d150947 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 926dc35fee8541f6b16da9b16e5649a523adb146..838356133cdb27c662176b908e2ae74ad75a946b 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 c3d7cc0adfbea54d2440ec4c844cf5b6717ffa52..5005af90d485da4e1238c4b1d9164d2d69a49372 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 71719917d0c4b23c3ce335690f7ae84a3f8f2c2e..7c03c8b72d4b15a11ee6e2642b9af2b711712706 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 a6d3ba9eb868de8b7e6b71723992fa52f87ae0c6..aabdfbf65e215fdca9470b4ab09304dfc1d4278f 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 04fe84683f3c0414daf7fc74af9f68b8e24f9594..e01668eabef11b2761f6b2f5b0ae933ad6865dd6 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 208f25a0e33a88d68977e193d22d1297b556a3ea..47197db39d3bf3db57112fff00c8db71d797bfa5 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 d79439592381040233d392cc164b882c83413b5d..08334bf1ec516312d9131efca2e5e5121d5039bb 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 8365f7118d5d78fc55908bda877998c069c4c333..0a811627600eada0727407e08df1b62fececb872 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 3aa6f6771cec924ff04bdf9f6d9f9dee82b2fcf5..e9927c1bf518236f4047c41df2d27dfcbc80704d 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 7b5e9953598d069a123e31f3b774811d53967624..f80e765ce302369fd6bd059a3ed94d6ec3829d8c 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 aad130cf2678159a7bd4fd03fe298d8b9a2f8990..3a51c1f26acea77e5586de6e9b357492d3b903dc 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 69a11dfaf3912f7d94cb63fecca5bf809c429ea5..acdf9b7eb5a5b9f201a7eae6e3a0878d30c61752 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 bdbad93ad04b5348c4ed7094f8340f18af63d847..48cae8a4fa9d2764f25f879d3bf6382b7b79c2dc 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 6cef449babf858d804745ec655497465c9b91d90..76c61c001baeb8c2fd51a9f564cfd8adea437391 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 76a0f993ea0a413033d6c7029f804d3bf4751763..903862cac6bb7dd200c0c87b12f188f9a85b29e6 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 b1c0dc37b4d1159237c9c8459bfb27e95507cfaa..e66418beeabf78d9c788e5997f6bd0876638e524 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 c6d9b007ad1348fe6d0559a188220faf25f4ef90..81766f4bd5524e0315f926b001d3e8a9d5b04c7e 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 7378b322426269ec6be8b79063ccfcc1522f75de..e7baea894f6ef0b63e683ac4792a4d1545256f9d 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 36e379d634d60d889f541997ef36a44dd354faa3..a27ac264a5c273adb79757d3581f8287e21d4cfb 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 b841abb754ddff19fb5c4f69cd1039421e578418..fe58e98cee5370a42c1aeb0e862d5d9e2304f6d1 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 cb5f2c53ea6af1366e9718de844510f12493083a..542cd586df0ed7971f773c944d830c0364dd88af 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 651957f53252bed8336dea6705153cadd3f177a4..4c2bf8bf001208ca52f37f760cf64582b7e78572 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 33e4b7db6819a4ee917c7a8a2991edcf22124519..0000000000000000000000000000000000000000 --- 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 3ee0c73a8d28c61b25c7489d73e0c8fd0785b955..294d087554eaf56bab98c47ffae12b192393ce56 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 64f9065be42f06d5cdb9febd28659d6935ae1607..89822246bb82b1073bd45f1efe4acda420febba1 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 531fd0e9c3205be10506ce5b7724afd5765b29e7..c75b44cc2fdcb840a09f7247b52cdf59605876b1 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 187356f0bf9493aecf546c1e382e9552151d157e..8b25f43ffc745d42dd80213aebf9a0eec5ba8ed0 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 115716bff6a0c030c2a96519f714b6fea03f289a..7c063fae045480771db5d9339ab448ddb2fe4067 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 0000000000000000000000000000000000000000..dfe24d1fb336eb730fa5b2e419d59de05c245691 --- /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 ded683f2ca1d278dd7553c3782e1f365cbc804ad..e9513725d9d882b0270ccb613b00eea0aac639ca 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 b1b35fdbd6cd0887e0682f9dd0580d2910f12159..76a821c7a17338ace56a57267c251df791b6512b 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 86b459e1866b3f797ae6fbaea2c3f72ab0720477..fd1829efe18ade355a7b861e66dd101cc5dca1f8 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 69d1ff3a39e2d1c35a58dbbed2739c7e3ee69aba..17631f2908dc5fe34f83452853b44a23df210ef8 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 34e88220b128b46df4e7f54e246e1aab8f3d42aa..813d363db00661fa45b980b9120c53eb505e2498 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 6e40dfdf3d88f620870550cbb479983ffa292fd6..5111b260e1c9f0565bda95429bc0f800fb914b89 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 d4b24d132995cf7a2a0a2e8edb98313f865ca757..de33a31b4110fcc9b9edcea0206d4d20a6521b41 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 8106934e86486404b5c6de3baa38b577128e1108..54e8f977a47d4230768c5704b22d577e4df6fbb6 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 e47dc6935d66defaa108b25b23aa43037f16ba87..0265c00a414d958dde4aeee29bef29bdf2f9c47d 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 0ee21f00fdc7d2ddd28cba99971ca7ef9c77f578..909007267b952ee3fbf7cf18273bee8df7d43c3f 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 7f7c18ddeb13ace9fa34d92d678c20dd1da69cdf..ff88e23682909b2aa9bf0c66f9fc4ecc8a0244b9 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 67c6cb737613c64aa5b28a6326a65f07ee41a434..d8042a9b7fc42cf87eec7f5f7cf7bf1251134ae6 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 ef99b2e92f06d72b0ae47e214480e19733e6013e..75fd1394a03fbc89d917734806575544edd1392a 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 d9261cda1b175fd95407ad53646ff434428f8968..ce626cf7b46c3f960dde979c0104ab6cc627c8bc 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 08f203a1bf6372bf3027cc9e260201570aa7112b..39cf3b5f8ae551fdb68803ef3a57f9bbcef62644 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 0ca5a9343f79639cbbbaa66fe6db926fe5af1f35..90935b9616b7ada4518c1a150bcaf5a1aefeaaa0 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 d6566dc4ec91f33dd48e1515fe7824e68a1b5677..0000000000000000000000000000000000000000 --- 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 34e010e0e8a139f12d6c7421d33b1647d68a0f93..3945a789c82835b3d14f571cf5cd92d99046b52d 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 20bcb1eeb23e66f9ed0331464677271838a6ccd7..d09b1c9d7f5e9fab87756ad84efd11fe19ee2e48 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 7809d4866f14ab6592db2f0b5819e412a64acca9..ba0af072716f14961457208e338c4d5d563248cf 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 fd101d43b5bad87d73be6fa4acb36788ec221c02..a24292a7c8c33c2a71066411e319d46c80432d1e 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 778ef01430e6a74c2291b951586f2e63aa453b31..181dcb7721f5002e0ebfd32bde68bb678cd360f5 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 a53cc27fac92de409d1c6d3b5e99a47bb341b487..4426169ef5add281df9404dce2df7e6e26bad658 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 3da1150f89b7c482addbe305919f2ca3c03197ad..27c47d368187c855635d69afd2609f53d1eab39c 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 100ace41f2aed1f56794388cf989498ad380f08b..305feaacaa15a4b38f01fbd2cd74836f4c130a45 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 9a72148f40801ae2e11adf4a93fac2b1bf6b1dc8..6b05e5bb4aa678bf9c3e39ce6351de91734d3353 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 0d5604aae6939706ef3f947497464746d8b0d132..551a66fbf3aba2b1f2d3a3135f884320ae9512be 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 948921efc0ba07a4edb1b902981b6adfda9da953..e4487dbcb8723d4d468c1d38a707e9a445be9a54 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 0ff3c3f547290b97a58da0d6113e2fa581c5a1ed..6cc1cc8e263f4362ca2891d702eb9f2f3c30e640 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 338496013a02a61954e865b003eaa9daf0377f0f..c09095b9849b6194dce1b1dbcba2be52ccb9891b 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 7345c91f67df3ba174c11001edf49c434b484297..348641e5ecb5a95ac8ff2ff746fb3b97eaf82251 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 ca04a17caa14b8b395b53ed1201beda33c1e79ae..1330399a8366b787cb3ed2e876a8f38c84558769 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 1cd50852e894a019fea77457464aaa5ccab33112..7ffde71c3b15118a2e093ce15951337ad0c6dede 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/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb new file mode 100644 index 0000000000000000000000000000000000000000..ca6dffe1cc57ac1f755547d9adc163f0826579f0 --- /dev/null +++ b/app/controllers/concerns/snippets_actions.rb @@ -0,0 +1,21 @@ +module SnippetsActions + extend ActiveSupport::Concern + + def edit + end + + def raw + send_data( + convert_line_endings(@snippet.content), + type: 'text/plain; charset=utf-8', + disposition: 'inline', + filename: @snippet.sanitized_file_name + ) + end + + private + + def convert_line_endings(content) + params[:line_ending] == 'raw' ? content : content.gsub(/\r\n/, "\n") + end +end diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index e3933e3d7b1bc49f8d6597e3d5de10c2ab19fc5e..4e61b0886d8746d3f20012126d819eac1f23f6df 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 a9a06ecc80801f30d380e69f3e08a0ba7bf18590..0d891ef4004ac3bca56d4a49f6a96ea05b7fe05d 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 db33b60b2294579d150ab3fe270955296c6c69ec..e2f81b09adc5dba3576ebe75d2ca3e3a5d69c86e 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 0ec8f5bd64a55b715d1b907a80dbc7ac0386a39f..fed75396d6e072088744718b7170bc8a0ad58f79 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 c3e1760f16864b07be779f49522cb4c5d3ce00d1..2bf3542d089e57eb047e013d1ccdad719d9496b1 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 74c54037ba9bcf2bd94beae5efba30c1f1b8c8c0..8b50ea207a5666337804910184b945a19ef13f1b 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/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 5d193f26a8e222efa51aa0e175ce114d331d3ca3..ef5d3d242eb3e6c50453048d883317c17b35f72e 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -1,6 +1,7 @@ class Projects::SnippetsController < Projects::ApplicationController include ToggleAwardEmoji include SpammableActions + include SnippetsActions before_action :module_enabled before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam] @@ -49,9 +50,6 @@ class Projects::SnippetsController < Projects::ApplicationController end end - def edit - end - def update UpdateSnippetService.new(project, current_user, @snippet, snippet_params).execute @@ -74,15 +72,6 @@ class Projects::SnippetsController < Projects::ApplicationController redirect_to namespace_project_snippets_path(@project.namespace, @project) end - def raw - send_data( - @snippet.content, - type: 'text/plain; charset=utf-8', - disposition: 'inline', - filename: @snippet.sanitized_file_name - ) - end - protected def snippet diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index b169d9936885130e93a3b8ef62b9bef735292903..366804ab17e6a3e8062bbf7429573adad5634c8b 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,6 +1,7 @@ class SnippetsController < ApplicationController include ToggleAwardEmoji include SpammableActions + include SnippetsActions before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] @@ -47,9 +48,6 @@ class SnippetsController < ApplicationController respond_with @snippet.becomes(Snippet) end - def edit - end - def update UpdateSnippetService.new(nil, current_user, @snippet, snippet_params).execute @@ -67,18 +65,9 @@ class SnippetsController < ApplicationController redirect_to snippets_path end - def raw - send_data( - @snippet.content, - type: 'text/plain; charset=utf-8', - disposition: 'inline', - filename: @snippet.sanitized_file_name - ) - end - def download send_data( - @snippet.content, + convert_line_endings(@snippet.content), type: 'text/plain; charset=utf-8', filename: @snippet.sanitized_file_name ) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 1576fc80a6b3b02a64571802765c0233a34898bd..206c92fe82af566b1ba93749b0febcd125124f9b 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 707eddd4d2991138ee6fb9d2f279e25e8ba48416..f542f72a386c6d5ad3ffbe094ae623e1f608d17d 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 8b82255445eaf088cbbb1aa4ca3f3ce93880a1a9..b76ca389f382ccebe4551ddccaa5376d74bd91e8 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 bee323993a09868bb6d8903d976cf4a6f0dbf839..6db813d4a02783c4ed0aa9e4de6300dc1ff3191b 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 e21178c7377139fa37ea4059f3b05aac52144e09..c1523b4dabf87202f0e461c1f5cac57306123417 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 7d4d049101adc0c7b8cb0dd2421159301ec2262f..3286a92a8a7d3023f860315c78070a797efe9f33 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 dd0a4ea03f0a6ff2bd5508554e2051c6a25e3ec9..c3a08d7631864b9cd7f81c233e56ad5331506c8c 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 60655bf2ea7d9b0f3f55d397e7dad39dea2e6ddb..bfba6425bfe4a83216544b0c192c8986823edab8 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 bbc358adb830a52a0504040ffde7b51725f588ca..dc4590a9923acef1bd418337d98bf431888e9475 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 ed1843ba0053e86a9f0c772005b3a088196b110a..07a086b0acae00b1a48cfdcad69cdf03572bb764 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 9547c57b2ae3374dd2b5f040cf07a074af09a193..99a6326309d0217056af2e45e2687ff4b05b1d61 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/concerns/spammable.rb b/app/models/concerns/spammable.rb index 423ae98a60e8206fcea6e2c03003b004a0c2341c..79adc77c9e42dd1894363d492b3762b4acb92beb 100644 --- a/app/models/concerns/spammable.rb +++ b/app/models/concerns/spammable.rb @@ -22,6 +22,10 @@ module Spammable delegate :ip_address, :user_agent, to: :user_agent_detail, allow_nil: true end + def submittable_as_spam_by?(current_user) + current_user && current_user.admin? && submittable_as_spam? + end + def submittable_as_spam? if user_agent_detail user_agent_detail.submittable? && current_application_settings.akismet_enabled diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 6de4d08fc2898697e7e70606e31d034f60ca0b1c..bd0336c984a4e2bea9e8fc2f29e20ca17c33e589 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -42,7 +42,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') } @@ -211,6 +211,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 @@ -224,7 +232,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 ed43fc2e57545c5963f07c2c3e36f3181f89f09f..fc5b1a66910f0be31da229b76dd670d0aae5c2d4 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 a03605d01fb5e507addb059755e0df1fa7b430cf..86d271a3f69df19fd60b3aa88152b7371d66b1cc 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 53e35cb21bf0692e3e83ac1e7520209ea1d67bb3..c776e0a20c411fb964786fddf00390d6e08cc4d9 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 14fd64e53321fe644ca98e2842e14e690c4ac6a8..b96aca47e65969d00ecae36347ffc5553b900a22 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 ab5e8b2416775b996bdd3b300bee23765af38fe4..5e5efca7bec285b3b39d07a30a99a64ec564da44 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 ca1d72070349c6b1a276fd053b114c71d01fdbfb..552113bac29f9e34430b3b4d8600b97974da1d8d 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 9db96347322848104d7a4623a91faf84708ee158..d0b991db11288ee0c42c67a0c3e29569645c0fbb 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 ad997ce2b131af6d4abb81cf2f62c03d1fbb9fec..f614eb66e1fe5f1af31140bf116347fd87c4559e 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 a559d0850c48b35dd84769e8795f214a55e7fda0..69bf693de8d8e42e7b1fee460b8a3d2c4cf13347 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 fe16a3784c4515a3832023df196580c727e7fa79..d0a60f134da45f017aee7e2de66b9fc164494fbc 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 1a2bad77a0291e6edfef7be253bdd5fdbed91262..fa45506317ec819a0bf20ebe6de9f3bba0d94338 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 0000000000000000000000000000000000000000..4b47ee489cf161e01cc040a45892a8d26d3ce3bd --- /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 0000000000000000000000000000000000000000..2c5e130e5aaa7b61482424e3a24eecca1079c864 --- /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 0000000000000000000000000000000000000000..450ee7da1c9a5d247f964ec61844e5aac8be8cc9 --- /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 7f2d28086f539a6a7adc3e4b24bfa142b8f56209..2e2d7f884acfb8bbe5ec8b6f3f442f123114ca34 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 12a8415d9a5f04f30aecbd8aed0d2c2c8c0f9aa3..727768b1a39482dfdb95dd8f13ca35a4745357a3 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 177b714b734a21f802f2c06e02424ab5d6351a95..3da1b657a41723fd04c513fa6204db5f2123fc6a 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -18,7 +18,7 @@ module MergeRequests @source = find_merge_source unless @source - log_merge_error('No source for merge', save_message_on_model: true) + return log_merge_error('No source for merge', save_message_on_model: true) end merge_request.in_locked_state do diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index a08c6fcd94b1504cf0be871d804cbd18907ef34c..9716a1780a9c17c09834103b810e822160b04057 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -17,8 +17,6 @@ module Projects def execute return false unless can?(current_user, :remove_project, project) - project.team.truncate - repo_path = project.path_with_namespace wiki_path = repo_path + '.wiki' @@ -30,6 +28,7 @@ module Projects Projects::UnlinkForkService.new(project, current_user).execute Project.transaction do + project.team.truncate project.destroy! unless remove_registry_tags diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 8ab943f4639cac3f691082654ccb83d4073c6264..ad86b4f9f4250967ebb7ad8b24a1e3e15001bd41 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 c4b748d0ab82d9935e867558bfce0cb33ae26427..6c48328da4f2d9a3838ad51d7099de265467c4fb 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 12b12fd248bbf6aaf5b9e214561d65f746fb5a5c..0287533ab2311256fa8fa167aa6c38c9dbeb501a 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 @@ -535,7 +535,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 4f982a6e3693de0e8a79da1fa7f618435c592af9..ac36bb5bb17c2dc35feced0b7b047600dd27dc9c 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 13d00dd1fcbcbd9c3dcdb6b5b370ee5afe960b7a..5e585ce789bc2da3833c25fc91ddc1b75285b33d 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 cdef63daca95d3c3f3c08d93e6d6996bb13886b2..c35945c5a357634ecd8d17099cc83d4ca1201967 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 721bc77cc2fa0f39bd364343a8bae1c67cf493cc..d725e477044555edbe65943bee1f1d46a3c41655 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 4dc44225d492bc1a42884f8bd4f363993e49c189..298cf0fa9509b9a83292543e6ddba2baf062028a 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 68a46f61eb759cfeccf41e256f20eaf9aa87a28b..ecdf76ef5c56229be683672fe5e2f36d9c2f455e 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 605bfd0cf8d02055e8e6ccac67476573f18fbf54..dc2d924f212a69d4e64049657a7c9dc8406249fe 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 c4bf2c90cc2a0b2a1bdb6a51a0285cb80e9d9fe6..16a5713948a9a901e1bad81d8fe35de9d66444ae 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 eddfce363a72c1737412c786d5b944d47463fa4e..da4769e214eea6adbcccf3e4e6b619a8856c77cd 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 8c4ad30c832ab80b5509149ee3bb6c11bb6aec79..dd34600490ed3abf3a5b61aedc7455377bc67ac8 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 64ca3c32e0109ec63ba9cdf3f93193f01a685e98..efd13aabf20743c1cf17628230a67b1f50c4e1aa 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 e30888484922673ebb190e771b7e0f4b52057cbe..56f463572bb824c1f76fadd528bbd7d759cd6fa1 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 cefe0066a8f4dd9bc7c9c566a8203767ee7434c9..5c5be03a7cd6c40fbd1db68e90a6722ed1945c15 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 f2d355587bde8b52406aa3aae2a17b561ead4881..302c1794628851efc90990b7cfdee39023975946 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 54d02ee8e4b961a799122f26dc6063e238b38f5d..a35a918d5011f599fe12dd227f7702c033f11913 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 248d439cd05bf94729e0e316a24b57695c111cff..19bd9b6d5c99ef7ba47aa938ff83396362be1040 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 59082ce5fd545bd57d927cc3c1a59ba0183dab70..ddf50d6667f7dbb6802d513367753c7972339cc9 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 205d23178d278234d272cdeedd9c8f40b5b8a644..5d4178f03d71310cc3045e17b1c2df057866cdfe 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 e5bda7b3a6ff5f493ae06b00fcf1dd9055fef123..3a1fcd00e9cef79a335addc14b8bf30ddbeea950 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 feadd863b00dd4f03e9796f2c4c3cf32e995c153..df0a0212f3d5b4518a87074f940472742bb47650 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -2,19 +2,6 @@ = render 'profiles/head' = 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 diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb index 8966dd3fd862fe8bc3dd4c4c20a6fcb84375fa86..431ab9d052b989fedd2bff41f6c568f17054dbc9 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 3b1a2e54ec2031282a2ac80c4dd939e58cf1239f..d1f7f65bf53a3d56717a3673cf07ad8db4ebb5e0 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 762ff34a9eccdf03f81821a0a51158a03a1eef06..b560ed21f1d3fc36c1f4e1cb66f8701ce29bd6b4 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 f852f2e3fd7bad2774b3c22bc330276bc14263f3..3475fa5f960908eec2c60a25cff78cdf6386f15b 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 b87b79b170ebd76f0224c5c9f7213599d9635ac8..5c38b5ad9c0444ebb3338537e03241491120e46b 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 074f1f634aed8b9544abe5c52aa44a714d39e884..997bf0fc5607c11b03e952fb5dc2cf68ab0f2635 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 2eea1db169a550e74553100e3255d4683fb54002..ebd1a914ee7b27d953f164d64bed46f866442c1b 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 0000000000000000000000000000000000000000..d9cb7bc033101bfeeae3cdf2fbf0cef55ccccb8a --- /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/issues/show.html.haml b/app/views/projects/issues/show.html.haml index d3eb3b7055b6dcf85a0ba8a409e4e6354e1714e4..069f3d979435edfe6b18988eec081948099ef8fc 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -40,7 +40,7 @@ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' %li = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) - - if @issue.submittable_as_spam? && current_user.admin? + - if @issue.submittable_as_spam_by?(current_user) %li = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam' @@ -50,7 +50,7 @@ - if can?(current_user, :update_issue, @issue) = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' - - if @issue.submittable_as_spam? && current_user.admin? + - if @issue.submittable_as_spam_by?(current_user) = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam' = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index e3062f47788ac6eccc2f3fd137e80f7ce8daacb2..c676953f729ab79cfd2a28e54cfd0d396a9f6abb 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 1b08165c14cdd3b1e3d1209a0694c7f38fe7b966..a73e8f345e07872056c6792c2e953a476312419a 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 a6cd2d83bd5b30fe1e1fce24b45e5f14ece74f95..e0c972aa2fb790f10e48d66a50a479afd0518304 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 81e393d7626117e500209c67db88bd3abee15b57..6e0428e2a31cc9b3b83ff75d224d108cb9ac912d 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/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index dde2e2b644d6e7ab4a9c564d79fd423ae8c5c168..34ee4ff1937d254f14201a4f5bd4fb390f89efa4 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -10,7 +10,7 @@ - if can?(current_user, :create_project_snippet, @project) = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do New snippet - - if @snippet.submittable_as_spam? && current_user.admin? + - if @snippet.submittable_as_spam_by?(current_user) = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) .visible-xs-block.dropdown @@ -31,6 +31,6 @@ %li = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do Edit - - if @snippet.submittable_as_spam? && current_user.admin? + - if @snippet.submittable_as_spam_by?(current_user) %li = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post diff --git a/app/views/projects/wikis/_sidebar_wiki_page.html.haml b/app/views/projects/wikis/_sidebar_wiki_page.html.haml index eb9bd14920d016c85e66874d5de70ff43bd8d997..0a61d90177be98cea964d2a1116c2ff1c91cadfe 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 8cbecb725b528aae1abd8caf9a455e5a7e4f0e63..5afb95ac4302538a87f2f241976b22b54b661367 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 f7808ea6aff9b963e6b7b55b0c5d798f71a42942..e977c1f169806751655eb0d9a561f2958faa2924 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 704d1d01a81fe9aa3f61265494b511dc79272ad0..026f404ce072dd1a1717a4e5925486fff1394283 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 ead9b84b991c80fce69c88c82b5f4cf79c625143..1744a597c5196b2473c806f5b3e397c79a040967 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 b6047ece5927b66b5ae113a44f4a6b542fa8381d..3baa956b910d818b3c887e2558fa579cf92bdf10 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 1154316c03f25a47741c71251ac5d098a8c9331f..ad995cbe9626f1048d0ac140f295f0a7c92749b2 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 31eb07ca666a5cef1150a30c939966bee4ec9f3c..a93cbd1041f6d966815dedafbfc03fc037b623ea 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/_actions.html.haml b/app/views/snippets/_actions.html.haml index 855a995afa931ae9a0709e8d7d9c7219b385d8bc..a7f118d3f7d1562c5631dfc31e9056032eef46a7 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -9,7 +9,7 @@ Delete = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do New snippet - - if @snippet.submittable_as_spam? && current_user.admin? + - if @snippet.submittable_as_spam_by?(current_user) = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } @@ -28,6 +28,6 @@ %li = link_to edit_snippet_path(@snippet) do Edit - - if @snippet.submittable_as_spam? && current_user.admin? + - if @snippet.submittable_as_spam_by?(current_user) %li = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml index 2dda5fed64794221d31c1cea59a480cd9b9a9faa..8b6a98a054a8fb16551a02567db861de0720f58c 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 diff --git a/changelogs/unreleased/21240_snippets_line_ending.yml b/changelogs/unreleased/21240_snippets_line_ending.yml new file mode 100644 index 0000000000000000000000000000000000000000..880fdd2c9ede1875e26a393a7f9dda8713245fb1 --- /dev/null +++ b/changelogs/unreleased/21240_snippets_line_ending.yml @@ -0,0 +1,4 @@ +--- +title: Download snippets with LF line-endings by default +merge_request: 8999 +author: diff --git a/changelogs/unreleased/22018-api-milestone-merge-requests.yml b/changelogs/unreleased/22018-api-milestone-merge-requests.yml new file mode 100644 index 0000000000000000000000000000000000000000..ccad2ec838ce415da268d2d345a6f94ef47c29fa --- /dev/null +++ b/changelogs/unreleased/22018-api-milestone-merge-requests.yml @@ -0,0 +1,4 @@ +--- +title: Adds API endpoint to fetch all merge request for a single milestone +merge_request: +author: Joren De Groof diff --git a/changelogs/unreleased/23819-fix-milestone-counters-to-top-right-of-panel-headings.yml b/changelogs/unreleased/23819-fix-milestone-counters-to-top-right-of-panel-headings.yml new file mode 100644 index 0000000000000000000000000000000000000000..628db8a5419e797407116b01e5fbb77931b8753d --- /dev/null +++ b/changelogs/unreleased/23819-fix-milestone-counters-to-top-right-of-panel-headings.yml @@ -0,0 +1,4 @@ +--- +title: Fix position of counter in milestone panels +merge_request: 7842 +author: Andrew Smith (EspadaV8) diff --git a/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml b/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml deleted file mode 100644 index 05fbd8f0bf2b2e14aaa95eda0749e14cc2dfe3f8..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms -merge_request: 8752 -author: diff --git a/changelogs/unreleased/25465-todo-done-clicking-is-kind-of-unsafe.yml b/changelogs/unreleased/25465-todo-done-clicking-is-kind-of-unsafe.yml new file mode 100644 index 0000000000000000000000000000000000000000..e9d46f6b12269308356ea81ca83302e38eb2e447 --- /dev/null +++ b/changelogs/unreleased/25465-todo-done-clicking-is-kind-of-unsafe.yml @@ -0,0 +1,4 @@ +--- +title: Todo done clicking is kind of unusable +merge_request: 8691 +author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/26379-iid-param.yml b/changelogs/unreleased/26379-iid-param.yml new file mode 100644 index 0000000000000000000000000000000000000000..ac743e68d6fc858a803d60c0c6d568eec249863b --- /dev/null +++ b/changelogs/unreleased/26379-iid-param.yml @@ -0,0 +1,4 @@ +--- +title: add :iids param to IssuableFinder (resolve technical dept) +merge_request: 9222 +author: mhasbini diff --git a/changelogs/unreleased/26500-informative-slack-notifications.yml b/changelogs/unreleased/26500-informative-slack-notifications.yml new file mode 100644 index 0000000000000000000000000000000000000000..342235424f4f43966738bce1a88c381249c49a0f --- /dev/null +++ b/changelogs/unreleased/26500-informative-slack-notifications.yml @@ -0,0 +1,4 @@ +--- +title: Add user & build links in Slack Notifications +merge_request: 8641 +author: Poornima M diff --git a/changelogs/unreleased/26824-diff-unfold-link-is-still-visible-when-there-are-no-lines-to-unfold.yml b/changelogs/unreleased/26824-diff-unfold-link-is-still-visible-when-there-are-no-lines-to-unfold.yml new file mode 100644 index 0000000000000000000000000000000000000000..182a9ae126b5589c8d0b243a5913a350632c04bd --- /dev/null +++ b/changelogs/unreleased/26824-diff-unfold-link-is-still-visible-when-there-are-no-lines-to-unfold.yml @@ -0,0 +1,5 @@ +--- +title: prevent diff unfolding link from appearing when there are no more lines to + show +merge_request: 8761 +author: diff --git a/changelogs/unreleased/26957-tanuki-anim-hang.yml b/changelogs/unreleased/26957-tanuki-anim-hang.yml new file mode 100644 index 0000000000000000000000000000000000000000..c7b4b9ebdfd697bf52fdea1af5704b523f961eea --- /dev/null +++ b/changelogs/unreleased/26957-tanuki-anim-hang.yml @@ -0,0 +1,4 @@ +--- +title: don't animate logo when downloading files +merge_request: +author: diff --git a/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml b/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml deleted file mode 100644 index 7b307b501f4bd906cf1441ec85226f345a1c20c7..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index -merge_request: 8956 -author: diff --git a/changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml b/changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml deleted file mode 100644 index 8f061a34ac0488aa3ff8174823d5dce0c7dc8119..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix filtered search user autocomplete for gitlab instances that are hosted - on a subdirectory -merge_request: 8891 -author: diff --git a/changelogs/unreleased/27452-update-issue-count.yml b/changelogs/unreleased/27452-update-issue-count.yml new file mode 100644 index 0000000000000000000000000000000000000000..a7417eba63c6b717712604b234d16b7c4ff59e32 --- /dev/null +++ b/changelogs/unreleased/27452-update-issue-count.yml @@ -0,0 +1,4 @@ +--- +title: update issue count when closing/reopening an issue +merge_request: +author: diff --git a/changelogs/unreleased/27480-deploy_keys_should_not_show_up_in_users_keys_list.yml b/changelogs/unreleased/27480-deploy_keys_should_not_show_up_in_users_keys_list.yml deleted file mode 100644 index 6e9192cb63291b122519f9c4ceb01d1ce9deef05..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/27480-deploy_keys_should_not_show_up_in_users_keys_list.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Do not display deploy keys in user's own ssh keys list -merge_request: 9024 -author: diff --git a/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml b/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml deleted file mode 100644 index bc990c668664ad5c1d743e4bdc76a692a1c288fb..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix wrong call to ProjectCacheWorker.perform -merge_request: 8910 -author: diff --git a/changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml b/changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml new file mode 100644 index 0000000000000000000000000000000000000000..89e2bdc69bc3fc934cff6b0e49d6eed242dacf52 --- /dev/null +++ b/changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml @@ -0,0 +1,4 @@ +--- +title: Only return target project's comments for a commit +merge_request: +author: diff --git a/changelogs/unreleased/27883-autocomplete-seems-to-not-trigger-when-at-character-is-part-of-an-autocompleted-text.yml b/changelogs/unreleased/27883-autocomplete-seems-to-not-trigger-when-at-character-is-part-of-an-autocompleted-text.yml new file mode 100644 index 0000000000000000000000000000000000000000..52b7e96682df52ff08b44db4fc89986030b442a3 --- /dev/null +++ b/changelogs/unreleased/27883-autocomplete-seems-to-not-trigger-when-at-character-is-part-of-an-autocompleted-text.yml @@ -0,0 +1,4 @@ +--- +title: Trigger autocomplete after selecting a slash command +merge_request: 9117 +author: diff --git a/changelogs/unreleased/27920-both-wip-messages-showing.yml b/changelogs/unreleased/27920-both-wip-messages-showing.yml new file mode 100644 index 0000000000000000000000000000000000000000..497fda8c8ba84a7c55b624f6a44f79c294890c4c --- /dev/null +++ b/changelogs/unreleased/27920-both-wip-messages-showing.yml @@ -0,0 +1,4 @@ +--- +title: Dispatch needed JS when creating a new MR in diff view +merge_request: +author: diff --git a/changelogs/unreleased/28059-add-pagination-to-admin-abuse-reports.yml b/changelogs/unreleased/28059-add-pagination-to-admin-abuse-reports.yml new file mode 100644 index 0000000000000000000000000000000000000000..1b2e678bbeda9542f562e8497992cbdc8ee0eadc --- /dev/null +++ b/changelogs/unreleased/28059-add-pagination-to-admin-abuse-reports.yml @@ -0,0 +1,4 @@ +--- +title: Restore pagination to admin abuse reports +merge_request: +author: diff --git a/changelogs/unreleased/28082-deleted-branch-event-404.yml b/changelogs/unreleased/28082-deleted-branch-event-404.yml new file mode 100644 index 0000000000000000000000000000000000000000..e989ca34784dfe004b667c853dc4e3723394cb55 --- /dev/null +++ b/changelogs/unreleased/28082-deleted-branch-event-404.yml @@ -0,0 +1,4 @@ +--- +title: Stop linking to deleted Branches in Activity tabs +merge_request: 9203 +author: Jan Christophersen diff --git a/changelogs/unreleased/28124-mrs-don-t-show-all-merge-errors.yml b/changelogs/unreleased/28124-mrs-don-t-show-all-merge-errors.yml deleted file mode 100644 index cd61c38e1bcb48fd0f046ef5fc68f0c59426c14e..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/28124-mrs-don-t-show-all-merge-errors.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Show merge errors in merge request widget -merge_request: 9229 -author: diff --git a/changelogs/unreleased/28176_merge_widget_fix.yml b/changelogs/unreleased/28176_merge_widget_fix.yml new file mode 100644 index 0000000000000000000000000000000000000000..8e4e75fc2372fc6c712db901af4c959f4796c552 --- /dev/null +++ b/changelogs/unreleased/28176_merge_widget_fix.yml @@ -0,0 +1,4 @@ +--- +title: Fix error in MR widget after /merge slash command +merge_request: 9259 +author: diff --git a/changelogs/unreleased/28236-browse-button-dropping.yml b/changelogs/unreleased/28236-browse-button-dropping.yml new file mode 100644 index 0000000000000000000000000000000000000000..3a3d755f40cc2c2e0a136881976aa32ada77e494 --- /dev/null +++ b/changelogs/unreleased/28236-browse-button-dropping.yml @@ -0,0 +1,4 @@ +--- +title: Increase right side of file header to button stays on same line +merge_request: +author: diff --git a/changelogs/unreleased/28247-timeloops-bug.yml b/changelogs/unreleased/28247-timeloops-bug.yml new file mode 100644 index 0000000000000000000000000000000000000000..12ab523b7c7e8f2690345e328095c47f81fa4ef3 --- /dev/null +++ b/changelogs/unreleased/28247-timeloops-bug.yml @@ -0,0 +1,4 @@ +--- +title: Only run timeago loops after rendering timeago components +merge_request: +author: diff --git a/changelogs/unreleased/28253-fix-buid-scroll-button-position.yml b/changelogs/unreleased/28253-fix-buid-scroll-button-position.yml new file mode 100644 index 0000000000000000000000000000000000000000..b13d115dab9304302afd443f428df524bea3c3d1 --- /dev/null +++ b/changelogs/unreleased/28253-fix-buid-scroll-button-position.yml @@ -0,0 +1,4 @@ +--- +title: Fix positioning of `Scroll to top` button +merge_request: +author: diff --git a/changelogs/unreleased/28262-horizontal-scrolling-issue-on-long-project-names.yml b/changelogs/unreleased/28262-horizontal-scrolling-issue-on-long-project-names.yml new file mode 100644 index 0000000000000000000000000000000000000000..fa1674453decc9dcafb2beb69a08143f8b657911 --- /dev/null +++ b/changelogs/unreleased/28262-horizontal-scrolling-issue-on-long-project-names.yml @@ -0,0 +1,4 @@ +--- +title: Wrap long Project and Group titles +merge_request: 9301 +author: diff --git a/changelogs/unreleased/28303-change-development-tanuki-favicon-colors-to-match-logo.yml b/changelogs/unreleased/28303-change-development-tanuki-favicon-colors-to-match-logo.yml new file mode 100644 index 0000000000000000000000000000000000000000..b97e9a59b2a78a8353873c6a7eeeec8d0c49a578 --- /dev/null +++ b/changelogs/unreleased/28303-change-development-tanuki-favicon-colors-to-match-logo.yml @@ -0,0 +1,4 @@ +--- +title: Change development tanuki favicon colors to match logo color order +merge_request: +author: diff --git a/changelogs/unreleased/28329-allow-slash-in-slash-command-args.yml b/changelogs/unreleased/28329-allow-slash-in-slash-command-args.yml new file mode 100644 index 0000000000000000000000000000000000000000..fed02139a5c4394c7a7f27b45e1bd84e4a3ab0f2 --- /dev/null +++ b/changelogs/unreleased/28329-allow-slash-in-slash-command-args.yml @@ -0,0 +1,4 @@ +--- +title: Allow slashes in slash command arguments +merge_request: +author: diff --git a/changelogs/unreleased/28353-little-grammar-issue.yml b/changelogs/unreleased/28353-little-grammar-issue.yml new file mode 100644 index 0000000000000000000000000000000000000000..10bdb17b266e8e1c209d593be0c0a54d35f3c906 --- /dev/null +++ b/changelogs/unreleased/28353-little-grammar-issue.yml @@ -0,0 +1,4 @@ +--- +title: Fix grammer issue in admin/runners +merge_request: +author: diff --git a/changelogs/unreleased/add-yarn-documentation.yml b/changelogs/unreleased/add-yarn-documentation.yml new file mode 100644 index 0000000000000000000000000000000000000000..5bcc01ac17747fcf6403a4d34df4de6036ba1369 --- /dev/null +++ b/changelogs/unreleased/add-yarn-documentation.yml @@ -0,0 +1,4 @@ +--- +title: add rake tasks to handle yarn dependencies and update documentation +merge_request: 9316 +author: diff --git a/changelogs/unreleased/api-fix-files.yml b/changelogs/unreleased/api-fix-files.yml deleted file mode 100644 index 8a9e29109a8d4c0d1c364b37aa42d299a4e93e92..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/api-fix-files.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Fix file downloading' -merge_request: Robert Schilling -author: 8267 diff --git a/changelogs/unreleased/api-post-block.yml b/changelogs/unreleased/api-post-block.yml new file mode 100644 index 0000000000000000000000000000000000000000..dfc61ffa9e3e36d806fa9beda8382f96ca2043b1 --- /dev/null +++ b/changelogs/unreleased/api-post-block.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Use POST to (un)block a user' +merge_request: 9371 +author: Robert Schilling diff --git a/changelogs/unreleased/api-remove-deploy-key-disable.yml b/changelogs/unreleased/api-remove-deploy-key-disable.yml new file mode 100644 index 0000000000000000000000000000000000000000..f471ad2aa20eb3f03b1d4417cc13a438b01ee2d6 --- /dev/null +++ b/changelogs/unreleased/api-remove-deploy-key-disable.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Remove `DELETE projects/:id/deploy_keys/:key_id/disable`' +merge_request: 9365 +author: Robert Schilling diff --git a/changelogs/unreleased/api-star-restful.yml b/changelogs/unreleased/api-star-restful.yml new file mode 100644 index 0000000000000000000000000000000000000000..3e7de8cd82273aa1836da533c69975c413ecef78 --- /dev/null +++ b/changelogs/unreleased/api-star-restful.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar`' +merge_request: 9328 +author: Robert Schilling diff --git a/changelogs/unreleased/change_queue_weight.yml b/changelogs/unreleased/change_queue_weight.yml new file mode 100644 index 0000000000000000000000000000000000000000..e4c650e8f79b11814f4a21eb61a37fb5a2f9cf30 --- /dev/null +++ b/changelogs/unreleased/change_queue_weight.yml @@ -0,0 +1,4 @@ +--- +title: Increase process_commit queue weight from 2 to 3 +merge_request: 9326 +author: blackst0ne diff --git a/changelogs/unreleased/dont-delete-assigned-issuables.yml b/changelogs/unreleased/dont-delete-assigned-issuables.yml deleted file mode 100644 index fb589a053c05d95ad78118126ee15011cf93580a..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/dont-delete-assigned-issuables.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't delete assigned MRs/issues when user is deleted -merge_request: -author: diff --git a/changelogs/unreleased/dynamic-project-title-fixture.yml b/changelogs/unreleased/dynamic-project-title-fixture.yml new file mode 100644 index 0000000000000000000000000000000000000000..2404cbb891cb813e5a245e77f6cd6a8f483dc522 --- /dev/null +++ b/changelogs/unreleased/dynamic-project-title-fixture.yml @@ -0,0 +1,4 @@ +--- +title: Replace static fixture for project_title_spec.js +merge_request: 9175 +author: winniehell diff --git a/changelogs/unreleased/fe-paginated-environments-api-add-subview.yml b/changelogs/unreleased/fe-paginated-environments-api-add-subview.yml new file mode 100644 index 0000000000000000000000000000000000000000..7e626982de6bfa520ccdf524988b049cd3241c0f --- /dev/null +++ b/changelogs/unreleased/fe-paginated-environments-api-add-subview.yml @@ -0,0 +1,4 @@ +--- +title: Adds paginationd and folders view to environments table +merge_request: +author: diff --git a/changelogs/unreleased/fix-anchor-scrolling.yml b/changelogs/unreleased/fix-anchor-scrolling.yml deleted file mode 100644 index 43b3b9bf96e14230b5779de9cdf0881be61c123d..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/fix-anchor-scrolling.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix broken anchor links when special characters are used -merge_request: 8961 -author: Andrey Krivko diff --git a/changelogs/unreleased/fix-gb-pipeline-retry-builds-started.yml b/changelogs/unreleased/fix-gb-pipeline-retry-builds-started.yml new file mode 100644 index 0000000000000000000000000000000000000000..49e243ca6bbbfbf5dd92f13261762c9eef9f964a --- /dev/null +++ b/changelogs/unreleased/fix-gb-pipeline-retry-builds-started.yml @@ -0,0 +1,4 @@ +--- +title: Fix CI/CD pipeline retry and take stages order into account +merge_request: 9021 +author: diff --git a/changelogs/unreleased/fix-gb-pipeline-retry-cancel-buttons-consistency.yml b/changelogs/unreleased/fix-gb-pipeline-retry-cancel-buttons-consistency.yml new file mode 100644 index 0000000000000000000000000000000000000000..d747e0e63a3f1fce00c9b78c62b4aeb503fbe166 --- /dev/null +++ b/changelogs/unreleased/fix-gb-pipeline-retry-cancel-buttons-consistency.yml @@ -0,0 +1,4 @@ +--- +title: Fix pipeline retry and cancel buttons on pipeline details page +merge_request: 9225 +author: diff --git a/changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml b/changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml new file mode 100644 index 0000000000000000000000000000000000000000..e09d03bb60853799e9c73fd5c50cb1219ece3723 --- /dev/null +++ b/changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml @@ -0,0 +1,4 @@ +--- +title: fix incorrect sidekiq concurrency count in admin background page +merge_request: +author: wendy0402 diff --git a/changelogs/unreleased/gfm-autocomplete-fixes.yml b/changelogs/unreleased/gfm-autocomplete-fixes.yml new file mode 100644 index 0000000000000000000000000000000000000000..737e2ad523436ff897f7d298ff557805be535034 --- /dev/null +++ b/changelogs/unreleased/gfm-autocomplete-fixes.yml @@ -0,0 +1,4 @@ +--- +title: Fix errors in slash commands matcher, add simple test coverage +merge_request: +author: YarNayar diff --git a/changelogs/unreleased/instrument-in-karma.yml b/changelogs/unreleased/instrument-in-karma.yml new file mode 100644 index 0000000000000000000000000000000000000000..cfabf2569fe7e85bf128b0da9bfd05b914cd0e3c --- /dev/null +++ b/changelogs/unreleased/instrument-in-karma.yml @@ -0,0 +1,4 @@ +--- +title: Move babel config for instanbul to karma config +merge_request: 9286 +author: winniehell diff --git a/changelogs/unreleased/only-yield-valid-reference-matches.yml b/changelogs/unreleased/only-yield-valid-reference-matches.yml new file mode 100644 index 0000000000000000000000000000000000000000..95da3cc56fd9323f9c872c26246fd22ea19687e6 --- /dev/null +++ b/changelogs/unreleased/only-yield-valid-reference-matches.yml @@ -0,0 +1,4 @@ +--- +title: Only yield valid references in ReferenceFilter.references_in +merge_request: +author: diff --git a/changelogs/unreleased/paginate-all-the-things.yml b/changelogs/unreleased/paginate-all-the-things.yml new file mode 100644 index 0000000000000000000000000000000000000000..52f23ba52a987f4d2994c72b149f036607cff1c6 --- /dev/null +++ b/changelogs/unreleased/paginate-all-the-things.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Paginate all endpoints that return an array' +merge_request: 8606 +author: Robert Schilling diff --git a/changelogs/unreleased/refresh-permissions-when-moving-projects.yml b/changelogs/unreleased/refresh-permissions-when-moving-projects.yml deleted file mode 100644 index a94bcdaa9a370b066f734b47f3452efcbc62a3eb..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/refresh-permissions-when-moving-projects.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Refresh authorizations when transferring projects -merge_request: -author: diff --git a/changelogs/unreleased/remove-issue-and-mr-counts-from-labels-page.yml b/changelogs/unreleased/remove-issue-and-mr-counts-from-labels-page.yml new file mode 100644 index 0000000000000000000000000000000000000000..b75b4644ba384e314322b1d5241bcdf1d4481e79 --- /dev/null +++ b/changelogs/unreleased/remove-issue-and-mr-counts-from-labels-page.yml @@ -0,0 +1,4 @@ +--- +title: Remove issue and MR counts from labels index +merge_request: +author: diff --git a/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml b/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml deleted file mode 100644 index f42aa6fae7984fc3decde22522253d50438bfc19..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't use backup Active Record connections for Sidekiq -merge_request: -author: diff --git a/changelogs/unreleased/seed-abuse-reports.yml b/changelogs/unreleased/seed-abuse-reports.yml new file mode 100644 index 0000000000000000000000000000000000000000..6fbcb81ae3ffd42ef35b06c84c0c48178de1839b --- /dev/null +++ b/changelogs/unreleased/seed-abuse-reports.yml @@ -0,0 +1,4 @@ +--- +title: Seed abuse reports for development +merge_request: +author: diff --git a/changelogs/unreleased/snippets-search-performance.yml b/changelogs/unreleased/snippets-search-performance.yml new file mode 100644 index 0000000000000000000000000000000000000000..2895478abfd0d3059d1f658da72721ffe67dd054 --- /dev/null +++ b/changelogs/unreleased/snippets-search-performance.yml @@ -0,0 +1,4 @@ +--- +title: Reduced query count for snippet search +merge_request: +author: diff --git a/changelogs/unreleased/task_list_refactor.yml b/changelogs/unreleased/task_list_refactor.yml new file mode 100644 index 0000000000000000000000000000000000000000..68942dadaa8f04e304fb7870a44453579d17d837 --- /dev/null +++ b/changelogs/unreleased/task_list_refactor.yml @@ -0,0 +1,4 @@ +--- +title: Deduplicate markdown task lists +merge_request: +author: diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index cc1af77a1def683cc237db778e35e99fe73c01a3..a82ff605a70dc591f9e87b969129ff3de30e9906 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -76,14 +76,6 @@ production: &base # default_can_create_group: false # default: true # username_changing_enabled: false # default: true - User can change her username/namespace - ## Default theme ID - ## 1 - Graphite - ## 2 - Charcoal - ## 3 - Green - ## 4 - Gray - ## 5 - Violet - ## 6 - Blue - # default_theme: 2 # default: 2 ## Automatic issue closing # If a commit message matches this regular expression, all issues referenced from the matched text will be closed. @@ -611,4 +603,4 @@ test: admin_group: '' staging: - <<: *base \ No newline at end of file + <<: *base diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index ab59394cb0c9c4a3f26bf13e601f39bb63a8a456..3f716dd8833dd4b21983b4709fd03bfb249be38c 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -183,7 +183,6 @@ Settings['gitlab'] ||= Settingslogic.new({}) Settings.gitlab['default_projects_limit'] ||= 10 Settings.gitlab['default_branch_protection'] ||= 2 Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil? -Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil? Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost' Settings.gitlab['ssh_host'] ||= Settings.gitlab.host Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? diff --git a/config/initializers/4_ci_app.rb b/config/initializers/4_ci_app.rb deleted file mode 100644 index d252e403102ca950ade8d76c3ae5289fc0a1b2e0..0000000000000000000000000000000000000000 --- a/config/initializers/4_ci_app.rb +++ /dev/null @@ -1,8 +0,0 @@ -module GitlabCi - VERSION = Gitlab::VERSION - REVISION = Gitlab::REVISION - - def self.config - Settings - end -end diff --git a/config/karma.config.js b/config/karma.config.js index 5b472780aed292ff23af63f4300109fef7ae072a..2f3cc9324131acde61cc0d28e1e3b8d0c2074102 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -2,6 +2,17 @@ var path = require('path'); var webpackConfig = require('./webpack.config.js'); var ROOT_PATH = path.resolve(__dirname, '..'); +// add coverage instrumentation to babel config +if (webpackConfig && webpackConfig.module && webpackConfig.module.rules) { + var babelConfig = webpackConfig.module.rules.find(function (rule) { + return rule.loader === 'babel-loader'; + }); + + babelConfig.options = babelConfig.options || {}; + babelConfig.options.plugins = babelConfig.options.plugins || []; + babelConfig.options.plugins.push('istanbul'); +} + // Karma configuration module.exports = function(config) { var progressReporter = process.env.CI ? 'mocha' : 'progress'; diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb index fb20c63bc631ea7b7ac899a385573a1e063247fd..adc3ad207cc79ef92a0c6d76421dba73d6086b9e 100644 --- a/config/routes/dashboard.rb +++ b/config/routes/dashboard.rb @@ -14,6 +14,9 @@ resource :dashboard, controller: 'dashboard', only: [] do collection do delete :destroy_all end + member do + patch :restore + end end resources :projects, only: [:index] do diff --git a/config/routes/project.rb b/config/routes/project.rb index 2ac98cf3842d62b90e10342157d720ef202be7fc..84f123ff7172c66e9bd1b0b71e4f3cfdfa4005de 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -156,6 +156,10 @@ constraints(ProjectUrlConstrainer.new) do get :terminal get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil } end + + collection do + get :folder, path: 'folders/:id' + end end resource :cycle_analytics, only: [:show] diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 56bf4e6b1de70258c6299a400b04eacb19ebabfa..97620cc9c7f2c91e558f06a06bd719c46e836e86 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -21,7 +21,7 @@ - [post_receive, 5] - [merge, 5] - [update_merge_requests, 3] - - [process_commit, 2] + - [process_commit, 3] - [new_note, 2] - [build, 2] - [pipeline, 2] diff --git a/config/webpack.config.js b/config/webpack.config.js index e63775e43ce770766f59ad7513e5f3fb467923c1..515569b2e97d1cd3ebe73b0691af99f12528cf09 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -22,6 +22,7 @@ var config = { commit_pipelines: './commit/pipelines/pipelines_bundle.js', diff_notes: './diff_notes/diff_notes_bundle.js', environments: './environments/environments_bundle.js', + environments_folder: './environments/folder/environments_folder_bundle.js', filtered_search: './filtered_search/filtered_search_bundle.js', graphs: './graphs/graphs_bundle.js', issuable: './issuable/issuable_bundle.js', @@ -54,18 +55,11 @@ var config = { exclude: /(node_modules|vendor\/assets)/, loader: 'babel-loader', options: { - plugins: IS_PRODUCTION ? [] : ['istanbul'], presets: [ ["es2015", {"modules": false}], 'stage-2' ] } - }, - { - test: /\.(js|es6)$/, - exclude: /node_modules/, - loader: 'imports-loader', - options: 'this=>window' } ] }, @@ -80,9 +74,6 @@ var config = { modules: false, assets: true }), - new CompressionPlugin({ - asset: '[path].gz[query]', - }), new webpack.IgnorePlugin(/moment/, /pikaday/), ], @@ -93,8 +84,7 @@ var config = { 'bootstrap/js': 'bootstrap-sass/assets/javascripts/bootstrap', 'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), - 'vue$': 'vue/dist/vue.js', - 'vue-resource$': 'vue-resource/dist/vue-resource.js' + 'vue$': IS_PRODUCTION ? 'vue/dist/vue.min.js' : 'vue/dist/vue.js', } } } @@ -102,7 +92,7 @@ var config = { if (IS_PRODUCTION) { config.devtool = 'source-map'; config.plugins.push( - new webpack.NoErrorsPlugin(), + new webpack.NoEmitOnErrorsPlugin(), new webpack.LoaderOptionsPlugin({ minimize: true, debug: false @@ -112,6 +102,9 @@ if (IS_PRODUCTION) { }), new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify('production') } + }), + new CompressionPlugin({ + asset: '[path].gz[query]', }) ); } diff --git a/db/fixtures/development/18_abuse_reports.rb b/db/fixtures/development/18_abuse_reports.rb new file mode 100644 index 0000000000000000000000000000000000000000..8618d10387a2a64e1a70cad8e4098ca493b4b981 --- /dev/null +++ b/db/fixtures/development/18_abuse_reports.rb @@ -0,0 +1,5 @@ +require 'factory_girl_rails' + +(AbuseReport.default_per_page + 3).times do + FactoryGirl.create(:abuse_report) +end diff --git a/db/migrate/20170210103609_add_index_to_user_agent_detail.rb b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb new file mode 100644 index 0000000000000000000000000000000000000000..c01753cfbd216721eaf7c7a88b4dd3b8e368be7f --- /dev/null +++ b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb @@ -0,0 +1,14 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddIndexToUserAgentDetail < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_concurrent_index(:user_agent_details, [:subject_id, :subject_type]) + end +end diff --git a/db/post_migrate/20170215200045_remove_theme_id_from_users.rb b/db/post_migrate/20170215200045_remove_theme_id_from_users.rb new file mode 100644 index 0000000000000000000000000000000000000000..c51646fbe52b6e19b7ccc3e8d1f4a7f6cd12ecaa --- /dev/null +++ b/db/post_migrate/20170215200045_remove_theme_id_from_users.rb @@ -0,0 +1,9 @@ +class RemoveThemeIdFromUsers < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + remove_column :users, :theme_id, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 29ba7167bfa9601781126469d05e7af0de575336..938e70ee57554d9714d9003a0046ba84cf7cefbc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170214111112) do +ActiveRecord::Schema.define(version: 20170215200045) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1219,6 +1219,8 @@ ActiveRecord::Schema.define(version: 20170214111112) do t.datetime "updated_at", null: false end + add_index "user_agent_details", ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree + create_table "users", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false @@ -1239,7 +1241,6 @@ ActiveRecord::Schema.define(version: 20170214111112) do t.string "linkedin", default: "", null: false t.string "twitter", default: "", null: false t.string "authentication_token" - t.integer "theme_id", default: 1, null: false t.string "bio" t.integer "failed_attempts", default: 0 t.datetime "locked_at" diff --git a/doc/README.md b/doc/README.md index 1943d656aa78624cfcf4da2b622f913921fa587e..2712206373d4c1b1a303f0340055c76f04f633d3 100644 --- a/doc/README.md +++ b/doc/README.md @@ -21,6 +21,7 @@ - [Profile Settings](profile/README.md) - [Project Services](user/project/integrations//project_services.md) Integrate a project with external services, such as CI and chat. - [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects. +- [Snippets](user/snippets.md) Snippets allow you to create little bits of code. - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. - [Webhooks](user/project/integrations/webhooks.md) Let GitLab notify you when new code has been pushed to your project. - [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 19316458d4902ddd8a8172845c44666f5857421c..8de0cc5af5c8c5c5169ac564d1414c3bc4d42651 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -1,4 +1,4 @@ -# GitLab Pages Administration +# GitLab Pages administration > **Notes:** - [Introduced][ee-80] in GitLab EE 8.3. @@ -6,6 +6,7 @@ - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17. - This guide is for Omnibus GitLab installations. If you have installed GitLab from source, follow the [Pages source installation document](source.md). +- To learn how to use GitLab Pages, read the [user documentation][pages-userguide]. --- @@ -14,9 +15,6 @@ sure to read the [changelog](#changelog) if you are upgrading to a new GitLab version as it may include new features and changes needed to be made in your configuration. -If you are looking for ways to upload your static content in GitLab Pages, you -probably want to read the [user documentation][pages-userguide]. - ## Overview GitLab Pages makes use of the [GitLab Pages daemon], a simple HTTP server @@ -32,7 +30,7 @@ In the case of custom domains, the Pages daemon needs to listen on ports `80` and/or `443`. For that reason, there is some flexibility in the way which you can set it up: -1. Run the pages daemon in the same server as GitLab, listening on a secondary IP +1. Run the pages daemon in the same server as GitLab, listening on a secondary IP. 1. Run the pages daemon in a separate server. In that case, the [Pages path](#change-storage-path) must also be present in the server that the pages daemon is installed, so you will have to share it via network. @@ -64,11 +62,11 @@ you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the host that GitLab runs. For example, an entry would look like this: ``` -*.example.io. 1800 IN A 1.2.3.4 +*.example.io. 1800 IN A 1.1.1.1 ``` where `example.io` is the domain under which GitLab Pages will be served -and `1.2.3.4` is the IP address of your GitLab instance. +and `1.1.1.1` is the IP address of your GitLab instance. > **Note:** You should not use the GitLab domain to serve user pages. For more information @@ -78,101 +76,126 @@ see the [security section](#security). ## Configuration -Depending on your needs, you can install GitLab Pages in four different ways. +Depending on your needs, you can set up GitLab Pages in 4 different ways. +The following options are listed from the easiest setup to the most +advanced one. The absolute minimum requirement is to set up the wildcard DNS +since that is needed in all configurations. -### Option 1. Custom domains with HTTPS support +### Wildcard domains -| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | -| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| `https://page.example.io` and `https://page.com` | yes | redirects to HTTPS | yes | yes | +>**Requirements:** +- [Wildcard DNS setup](#dns-configuration) +> +>--- +> +URL scheme: `http://page.example.io` -Pages enabled, daemon is enabled AND pages has external IP support enabled. -In that case, the pages daemon is running, NGINX still proxies requests to -the daemon but the daemon is also able to receive requests from the outside -world. Custom domains and TLS are supported. +This is the minimum setup that you can use Pages with. It is the base for all +other setups as described below. Nginx will proxy all requests to the daemon. +The Pages daemon doesn't listen to the outside world. -1. Edit `/etc/gitlab/gitlab.rb`: +1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`: ```ruby - pages_external_url "https://example.io" - nginx['listen_addresses'] = ['1.1.1.1'] - pages_nginx['enable'] = false - gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt" - gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key" - gitlab_pages['external_http'] = '1.1.1.2:80' - gitlab_pages['external_https'] = '1.1.1.2:443' + pages_external_url 'http://example.io' ``` - where `1.1.1.1` is the primary IP address that GitLab is listening to and - `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. - 1. [Reconfigure GitLab][reconfigure] -### Option 2. Custom domains without HTTPS support +### Wildcard domains with TLS support -| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | -| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| `http://page.example.io` and `http://page.com` | no | yes | no | yes | +>**Requirements:** +- [Wildcard DNS setup](#dns-configuration) +- Wildcard TLS certificate +> +>--- +> +URL scheme: `https://page.example.io` -Pages enabled, daemon is enabled AND pages has external IP support enabled. -In that case, the pages daemon is running, NGINX still proxies requests to -the daemon but the daemon is also able to receive requests from the outside -world. Custom domains and TLS are supported. +Nginx will proxy all requests to the daemon. Pages daemon doesn't listen to the +outside world. -1. Edit `/etc/gitlab/gitlab.rb`: +1. Place the certificate and key inside `/etc/gitlab/ssl` +1. In `/etc/gitlab/gitlab.rb` specify the following configuration: ```ruby - pages_external_url "http://example.io" - nginx['listen_addresses'] = ['1.1.1.1'] - pages_nginx['enable'] = false - gitlab_pages['external_http'] = '1.1.1.2:80' + pages_external_url 'https://example.io' + + pages_nginx['redirect_http_to_https'] = true + pages_nginx['ssl_certificate'] = "/etc/gitlab/ssl/pages-nginx.crt" + pages_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/pages-nginx.key" ``` - where `1.1.1.1` is the primary IP address that GitLab is listening to and - `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. + where `pages-nginx.crt` and `pages-nginx.key` are the SSL cert and key, + respectively. 1. [Reconfigure GitLab][reconfigure] -### Option 3. Wildcard HTTPS domain without custom domains +## Advanced configuration -| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | -| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| `https://page.example.io` | yes | no | no | no | +In addition to the wildcard domains, you can also have the option to configure +GitLab Pages to work with custom domains. Again, there are two options here: +support custom domains with and without TLS certificates. The easiest setup is +that without TLS certificates. -Pages enabled, daemon is enabled and NGINX will proxy all requests to the -daemon. Pages daemon doesn't listen to the outside world. +### Custom domains -1. Place the certificate and key inside `/etc/gitlab/ssl` -1. In `/etc/gitlab/gitlab.rb` specify the following configuration: +>**Requirements:** +- [Wildcard DNS setup](#dns-configuration) +- Secondary IP +> +--- +> +URL scheme: `http://page.example.io` and `http://domain.com` - ```ruby - pages_external_url 'https://example.io' +In that case, the pages daemon is running, Nginx still proxies requests to +the daemon but the daemon is also able to receive requests from the outside +world. Custom domains are supported, but no TLS. - pages_nginx['redirect_http_to_https'] = true - pages_nginx['ssl_certificate'] = "/etc/gitlab/ssl/pages-nginx.crt" - pages_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/pages-nginx.key" +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + pages_external_url "http://example.io" + nginx['listen_addresses'] = ['1.1.1.1'] + pages_nginx['enable'] = false + gitlab_pages['external_http'] = '1.1.1.2:80' ``` - where `pages-nginx.crt` and `pages-nginx.key` are the SSL cert and key, - respectively. + where `1.1.1.1` is the primary IP address that GitLab is listening to and + `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. 1. [Reconfigure GitLab][reconfigure] -### Option 4. Wildcard HTTP domain without custom domains +### Custom domains with TLS support -| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | -| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| `http://page.example.io` | no | no | no | no | +>**Requirements:** +- [Wildcard DNS setup](#dns-configuration) +- Wildcard TLS certificate +- Secondary IP +> +--- +> +URL scheme: `https://page.example.io` and `https://domain.com` -Pages enabled, daemon is enabled and NGINX will proxy all requests to the -daemon. Pages daemon doesn't listen to the outside world. +In that case, the pages daemon is running, Nginx still proxies requests to +the daemon but the daemon is also able to receive requests from the outside +world. Custom domains and TLS are supported. -1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`: +1. Edit `/etc/gitlab/gitlab.rb`: ```ruby - pages_external_url 'http://example.io' + pages_external_url "https://example.io" + nginx['listen_addresses'] = ['1.1.1.1'] + pages_nginx['enable'] = false + gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt" + gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key" + gitlab_pages['external_http'] = '1.1.1.2:80' + gitlab_pages['external_https'] = '1.1.1.2:443' ``` + where `1.1.1.1` is the primary IP address that GitLab is listening to and + `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. + 1. [Reconfigure GitLab][reconfigure] ## Change storage path diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md index 0718c05a6f4e55508aa58b0d6815add51a4917d9..463715e48cab77080dda461ea52c6b72dd8fad7a 100644 --- a/doc/administration/pages/source.md +++ b/doc/administration/pages/source.md @@ -17,22 +17,54 @@ Pages to the latest supported version. ## Prerequisites -[Read the Omnibus prerequisites section.](index.md#prerequisites) +Before proceeding with the Pages configuration, you will need to: + +1. Have a separate domain under which the GitLab Pages will be served. In this + document we assume that to be `example.io`. +1. Configure a **wildcard DNS record**. +1. (Optional) Have a **wildcard certificate** for that domain if you decide to + serve Pages under HTTPS. +1. (Optional but recommended) Enable [Shared runners](../../ci/runners/README.md) + so that your users don't have to bring their own. + +### DNS configuration + +GitLab Pages expect to run on their own virtual host. In your DNS server/provider +you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the +host that GitLab runs. For example, an entry would look like this: + +``` +*.example.io. 1800 IN A 1.1.1.1 +``` + +where `example.io` is the domain under which GitLab Pages will be served +and `1.1.1.1` is the IP address of your GitLab instance. + +> **Note:** +You should not use the GitLab domain to serve user pages. For more information +see the [security section](#security). + +[wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record ## Configuration -Depending on your needs, you can install GitLab Pages in four different ways. +Depending on your needs, you can set up GitLab Pages in 4 different ways. +The following options are listed from the easiest setup to the most +advanced one. The absolute minimum requirement is to set up the wildcard DNS +since that is needed in all configurations. -### Option 1. Custom domains with HTTPS support +### Wildcard domains -| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | -| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| `https://page.example.io` and `https://page.com` | yes | redirects to HTTPS | yes | yes | +>**Requirements:** +- [Wildcard DNS setup](#dns-configuration) +> +>--- +> +URL scheme: `http://page.example.io` -Pages enabled, daemon is enabled AND pages has external IP support enabled. -In that case, the pages daemon is running, NGINX still proxies requests to -the daemon but the daemon is also able to receive requests from the outside -world. Custom domains and TLS are supported. +This is the minimum setup that you can use Pages with. It is the base for all +other setups as described below. Nginx will proxy all requests to the daemon. +The Pages daemon doesn't listen to the outside world. 1. Install the Pages daemon: @@ -44,10 +76,14 @@ world. Custom domains and TLS are supported. sudo -u git -H make ``` -1. Edit `gitlab.yml` to look like the example below. You need to change the - `host` to the FQDN under which GitLab Pages will be served. Set - `external_http` and `external_https` to the secondary IP on which the pages - daemon will listen for connections: +1. Go to the GitLab installation directory: + + ```bash + cd /home/git/gitlab + ``` + +1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true` and + the `host` to the FQDN under which GitLab Pages will be served: ```yaml ## GitLab Pages @@ -57,25 +93,10 @@ world. Custom domains and TLS are supported. # path: shared/pages host: example.io - port: 443 - https: true - - external_http: 1.1.1.2:80 - external_https: 1.1.1.2:443 + port: 80 + https: false ``` -1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in - order to enable the pages daemon. In `gitlab_pages_options` the - `-pages-domain`, `-listen-http` and `-listen-https` must match the `host`, - `external_http` and `external_https` settings that you set above respectively. - The `-root-cert` and `-root-key` settings are the wildcard TLS certificates - of the `example.io` domain: - - ``` - gitlab_pages_enabled=true - gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80 -listen-https 1.1.1.2:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key - ``` - 1. Copy the `gitlab-pages-ssl` Nginx configuration file: ```bash @@ -85,22 +106,21 @@ world. Custom domains and TLS are supported. Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. -1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace - `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab - listens to. 1. Restart NGINX 1. [Restart GitLab][restart] -### Option 2. Custom domains without HTTPS support +### Wildcard domains with TLS support -| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | -| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| `http://page.example.io` and `http://page.com` | no | yes | no | yes | +>**Requirements:** +- [Wildcard DNS setup](#dns-configuration) +- Wildcard TLS certificate +> +>--- +> +URL scheme: `https://page.example.io` -Pages enabled, daemon is enabled AND pages has external IP support enabled. -In that case, the pages daemon is running, NGINX still proxies requests to -the daemon but the daemon is also able to receive requests from the outside -world. Custom domains and TLS are supported. +Nginx will proxy all requests to the daemon. Pages daemon doesn't listen to the +outside world. 1. Install the Pages daemon: @@ -112,34 +132,20 @@ world. Custom domains and TLS are supported. sudo -u git -H make ``` -1. Edit `gitlab.yml` to look like the example below. You need to change the - `host` to the FQDN under which GitLab Pages will be served. Set - `external_http` to the secondary IP on which the pages daemon will listen - for connections: +1. In `gitlab.yml`, set the port to `443` and https to `true`: - ```yaml + ```bash + ## GitLab Pages pages: enabled: true # The location where pages are stored (default: shared/pages). # path: shared/pages host: example.io - port: 80 - https: false - - external_http: 1.1.1.2:80 + port: 443 + https: true ``` -1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in - order to enable the pages daemon. In `gitlab_pages_options` the - `-pages-domain` and `-listen-http` must match the `host` and `external_http` - settings that you set above respectively: - - ``` - gitlab_pages_enabled=true - gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80" - ``` - 1. Copy the `gitlab-pages-ssl` Nginx configuration file: ```bash @@ -149,20 +155,30 @@ world. Custom domains and TLS are supported. Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. -1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace - `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab - listens to. 1. Restart NGINX 1. [Restart GitLab][restart] -### Option 3. Wildcard HTTPS domain without custom domains -| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | -| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| `https://page.example.io` | yes | no | no | no | +## Advanced configuration + +In addition to the wildcard domains, you can also have the option to configure +GitLab Pages to work with custom domains. Again, there are two options here: +support custom domains with and without TLS certificates. The easiest setup is +that without TLS certificates. + +### Custom domains -Pages enabled, daemon is enabled and NGINX will proxy all requests to the -daemon. Pages daemon doesn't listen to the outside world. +>**Requirements:** +- [Wildcard DNS setup](#dns-configuration) +- Secondary IP +> +--- +> +URL scheme: `http://page.example.io` and `http://domain.com` + +In that case, the pages daemon is running, Nginx still proxies requests to +the daemon but the daemon is also able to receive requests from the outside +world. Custom domains are supported, but no TLS. 1. Install the Pages daemon: @@ -173,20 +189,35 @@ daemon. Pages daemon doesn't listen to the outside world. sudo -u git -H git checkout v0.2.4 sudo -u git -H make ``` -1. In `gitlab.yml`, set the port to `443` and https to `true`: - ```bash - ## GitLab Pages +1. Edit `gitlab.yml` to look like the example below. You need to change the + `host` to the FQDN under which GitLab Pages will be served. Set + `external_http` to the secondary IP on which the pages daemon will listen + for connections: + + ```yaml pages: enabled: true # The location where pages are stored (default: shared/pages). # path: shared/pages host: example.io - port: 443 - https: true + port: 80 + https: false + + external_http: 1.1.1.2:80 ``` +1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in + order to enable the pages daemon. In `gitlab_pages_options` the + `-pages-domain` and `-listen-http` must match the `host` and `external_http` + settings that you set above respectively: + + ``` + gitlab_pages_enabled=true + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80" + ``` + 1. Copy the `gitlab-pages-ssl` Nginx configuration file: ```bash @@ -196,17 +227,26 @@ daemon. Pages daemon doesn't listen to the outside world. Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. +1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace + `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab + listens to. 1. Restart NGINX 1. [Restart GitLab][restart] -### Option 4. Wildcard HTTP domain without custom domains +### Custom domains with TLS support -| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | -| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| `http://page.example.io` | no | no | no | no | +>**Requirements:** +- [Wildcard DNS setup](#dns-configuration) +- Wildcard TLS certificate +- Secondary IP +> +--- +> +URL scheme: `https://page.example.io` and `https://domain.com` -Pages enabled, daemon is enabled and NGINX will proxy all requests to the -daemon. Pages daemon doesn't listen to the outside world. +In that case, the pages daemon is running, Nginx still proxies requests to +the daemon but the daemon is also able to receive requests from the outside +world. Custom domains and TLS are supported. 1. Install the Pages daemon: @@ -218,14 +258,10 @@ daemon. Pages daemon doesn't listen to the outside world. sudo -u git -H make ``` -1. Go to the GitLab installation directory: - - ```bash - cd /home/git/gitlab - ``` - -1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true` and - the `host` to the FQDN under which GitLab Pages will be served: +1. Edit `gitlab.yml` to look like the example below. You need to change the + `host` to the FQDN under which GitLab Pages will be served. Set + `external_http` and `external_https` to the secondary IP on which the pages + daemon will listen for connections: ```yaml ## GitLab Pages @@ -235,10 +271,25 @@ daemon. Pages daemon doesn't listen to the outside world. # path: shared/pages host: example.io - port: 80 - https: false + port: 443 + https: true + + external_http: 1.1.1.2:80 + external_https: 1.1.1.2:443 ``` +1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in + order to enable the pages daemon. In `gitlab_pages_options` the + `-pages-domain`, `-listen-http` and `-listen-https` must match the `host`, + `external_http` and `external_https` settings that you set above respectively. + The `-root-cert` and `-root-key` settings are the wildcard TLS certificates + of the `example.io` domain: + + ``` + gitlab_pages_enabled=true + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80 -listen-https 1.1.1.2:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key + ``` + 1. Copy the `gitlab-pages-ssl` Nginx configuration file: ```bash @@ -248,9 +299,27 @@ daemon. Pages daemon doesn't listen to the outside world. Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. +1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace + `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab + listens to. 1. Restart NGINX 1. [Restart GitLab][restart] +## Change storage path + +Follow the steps below to change the default path where GitLab Pages' contents +are stored. + +1. Pages are stored by default in `/var/opt/gitlab/gitlab-rails/shared/pages`. + If you wish to store them in another location you must set it up in + `/etc/gitlab/gitlab.rb`: + + ```ruby + gitlab_rails['pages_path'] = "/mnt/storage/pages" + ``` + +1. [Reconfigure GitLab][reconfigure] + ## NGINX caveats >**Note:** diff --git a/doc/api/commits.md b/doc/api/commits.md index 3223b82f60a42c4f16bedfe54657176a0bff5ce3..eaab3e0df3d25f0f00bb0846fd0d4d3594a1b3c4 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -12,8 +12,8 @@ GET /projects/:id/repository/commits | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch | -| `since` | string | no | Only commits after or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | -| `until` | string | no | Only commits before or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | +| `since` | string | no | Only commits after or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | +| `until` | string | no | Only commits before or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits" diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md index 284d5f88c55e2b704dee9b3133236b556e9c0dca..39afc4b2df510400a285e8ef9cab232e9f5d87c5 100644 --- a/doc/api/deploy_keys.md +++ b/doc/api/deploy_keys.md @@ -137,7 +137,7 @@ Example response: ## Delete deploy key -Delete a deploy key from a project +Removes a deploy key from the project. If the deploy key is used only for this project, it will be deleted from the system. ``` DELETE /projects/:id/deploy_keys/:key_id @@ -156,14 +156,11 @@ Example response: ```json { - "updated_at" : "2015-08-29T12:50:57.259Z", - "key" : "ssh-rsa AAAA...", - "public" : false, - "title" : "My deploy key", - "user_id" : null, - "created_at" : "2015-08-29T12:50:57.259Z", - "fingerprint" : "6a:33:1f:74:51:c0:39:81:79:ec:7a:31:f8:40:20:43", - "id" : 13 + "id": 6, + "deploy_key_id": 14, + "project_id": 1, + "created_at" : "2015-08-29T12:50:57.259Z", + "updated_at" : "2015-08-29T12:50:57.259Z" } ``` @@ -190,27 +187,3 @@ Example response: "created_at" : "2015-08-29T12:44:31.550Z" } ``` - -## Disable a deploy key - -Disable a deploy key for a project. Returns the disabled key. - -```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/disable -``` - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | -| `key_id` | integer | yes | The ID of the deploy key | - -Example response: - -```json -{ - "key" : "ssh-rsa AAAA...", - "id" : 12, - "title" : "My deploy key", - "created_at" : "2015-08-29T12:44:31.550Z" -} -``` diff --git a/doc/api/keys.md b/doc/api/keys.md index b68f08a007d53dc81ffc82aca1de8aecddd46bbf..3b55c2baf568a25a55e058170a4a97ef781e2b71 100644 --- a/doc/api/keys.md +++ b/doc/api/keys.md @@ -33,7 +33,6 @@ Parameters: "twitter": "", "website_url": "", "email": "john@example.com", - "theme_id": 2, "color_scheme_id": 1, "projects_limit": 10, "current_sign_in_at": null, diff --git a/doc/api/milestones.md b/doc/api/milestones.md index 12497acff9845b44e6a8cf9ce6179ebc41dc8585..bf7dcc008e96c2b611640a464e4329ab9ddc6974 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -103,3 +103,16 @@ Parameters: - `id` (required) - The ID of a project - `milestone_id` (required) - The ID of a project milestone + +## Get all merge requests assigned to a single milestone + +Gets all merge requests assigned to a single project milestone. + +``` +GET /projects/:id/milestones/:milestone_id/merge_requests +``` + +Parameters: + +- `id` (required) - The ID of a project +- `milestone_id` (required) - The ID of a project milestone \ No newline at end of file diff --git a/doc/api/projects.md b/doc/api/projects.md index b3136be67316184c95498dbb2058368e36810c75..e9ef03a0c0c610f91ac2e53123e7a6e67b36962c 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -609,7 +609,7 @@ Example response: Unstars a given project. Returns status code `304` if the project is not starred. ``` -DELETE /projects/:id/star +POST /projects/:id/unstar ``` | Attribute | Type | Required | Description | @@ -617,7 +617,7 @@ DELETE /projects/:id/star | `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/unstar" ``` Example response: @@ -1194,4 +1194,4 @@ Parameters: | --------- | ---- | -------- | ----------- | | `query` | string | yes | A string contained in the project name | | `order_by` | string | no | Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields | -| `sort` | string | no | Return requests sorted in `asc` or `desc` order | \ No newline at end of file +| `sort` | string | no | Return requests sorted in `asc` or `desc` order | diff --git a/doc/api/session.md b/doc/api/session.md index f776424023e8eacebc9fcfd9c36914ececdcc447..d7809716fbe3d99a0f93bf83697a4bb423988aa5 100644 --- a/doc/api/session.md +++ b/doc/api/session.md @@ -41,7 +41,6 @@ Example response: "twitter": "", "website_url": "", "email": "john@example.com", - "theme_id": 1, "color_scheme_id": 1, "projects_limit": 10, "current_sign_in_at": "2015-07-07T07:10:58.392Z", diff --git a/doc/api/users.md b/doc/api/users.md index ed3469521fc9d0b72924cc56fa5d27f9d88d348f..852c7ac8ec2cc664a8df0b1c7ab8f5f2f67cbe2f 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -72,7 +72,6 @@ GET /users "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", - "theme_id": 1, "color_scheme_id": 2, "projects_limit": 100, "current_sign_in_at": "2012-06-02T06:36:55Z", @@ -105,7 +104,6 @@ GET /users "organization": "", "last_sign_in_at": null, "confirmed_at": "2012-05-30T16:53:06.148Z", - "theme_id": 1, "color_scheme_id": 3, "projects_limit": 100, "current_sign_in_at": "2014-03-19T17:54:13Z", @@ -198,7 +196,6 @@ Parameters: "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", - "theme_id": 1, "color_scheme_id": 2, "projects_limit": 100, "current_sign_in_at": "2012-06-02T06:36:55Z", @@ -323,7 +320,6 @@ GET /user "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", - "theme_id": 1, "color_scheme_id": 2, "projects_limit": 100, "current_sign_in_at": "2012-06-02T06:36:55Z", @@ -369,7 +365,6 @@ GET /user "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", - "theme_id": 1, "color_scheme_id": 2, "projects_limit": 100, "current_sign_in_at": "2012-06-02T06:36:55Z", @@ -664,14 +659,14 @@ Will return `200 OK` on success, or `404 Not found` if either user or email cann Blocks the specified user. Available only for admin. ``` -PUT /users/:id/block +POST /users/:id/block ``` Parameters: - `id` (required) - id of specified user -Will return `200 OK` on success, `404 User Not Found` is user cannot be found or +Will return `201 OK` on success, `404 User Not Found` is user cannot be found or `403 Forbidden` when trying to block an already blocked user by LDAP synchronization. ## Unblock user @@ -679,14 +674,14 @@ Will return `200 OK` on success, `404 User Not Found` is user cannot be found or Unblocks the specified user. Available only for admin. ``` -PUT /users/:id/unblock +POST /users/:id/unblock ``` Parameters: - `id` (required) - id of specified user -Will return `200 OK` on success, `404 User Not Found` is user cannot be found or +Will return `201 OK` on success, `404 User Not Found` is user cannot be found or `403 Forbidden` when trying to unblock a user blocked by LDAP synchronization. ### Get user contribution events diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 84ff72bc36c601f669dc2dd49c35178f652e6f7d..49f140a37f69016b761bb0bb0100047f52966504 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -13,6 +13,7 @@ changes are in V4: - Project snippets do not return deprecated field `expires_at` - Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`) - Status 409 returned for POST `project/:id/members` when a member already exists +- Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar` - Removed the following deprecated Templates endpoints (these are still accessible with `/templates` prefix) - `/licences` - `/licences/:key` @@ -24,3 +25,6 @@ changes are in V4: - `/dockerfiles/:key` - Moved `/projects/fork/:id` to `/projects/:id/fork` - Endpoints `/projects/owned`, `/projects/visible`, `/projects/starred` & `/projects/all` are consolidated into `/projects` using query parameters +- Return pagination headers for all endpoints that return an array +- Removed `DELETE projects/:id/deploy_keys/:key_id/disable`. Use `DELETE projects/:id/deploy_keys/:key_id` instead +- Moved `PUT /users/:id/(block|unblock)` to `POST /users/:id/(block|unblock)` diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index 9d294240d9d04c2afbac8f4019b785c03ef0626d..db92a4b0d8051e7ee937f408c88178df728e1c37 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -91,7 +91,7 @@ total running time should be: ## Badges -Job status and test coverage report badges are available. You can find their +Pipeline status and test coverage report badges are available. You can find their respective link in the [Pipelines settings] page. [jobs]: #jobs diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md index 0f78e8238af6bf8ce70ff8a3fbf4bdd54bb73b58..565d4b33457dc7757927bb0ea690cba9a0df018b 100644 --- a/doc/development/gotchas.md +++ b/doc/development/gotchas.md @@ -3,7 +3,7 @@ The purpose of this guide is to document potential "gotchas" that contributors might encounter or should avoid during development of GitLab CE and EE. -## Don't `describe` symbols +## Do not `describe` symbols Consider the following model spec: @@ -32,7 +32,7 @@ spec/models/user_spec.rb|6 error| Failure/Error: u = described_class.new NoMeth Except for the top-level `describe` block, always provide a String argument to `describe`. -## Don't assert against the absolute value of a sequence-generated attribute +## Do not assert against the absolute value of a sequence-generated attribute Consider the following factory: @@ -121,7 +121,7 @@ describe API::Labels do end ``` -## Don't `rescue Exception` +## Do not `rescue Exception` See ["Why is it bad style to `rescue Exception => e` in Ruby?"][Exception]. @@ -130,7 +130,7 @@ Rubocop](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/.rubocop.yml#L9 [Exception]: http://stackoverflow.com/q/10048173/223897 -## Don't use inline JavaScript in views +## Do not use inline JavaScript in views Using the inline `:javascript` Haml filters comes with a performance overhead. Using inline JavaScript is not a good way to structure your code and should be avoided. diff --git a/doc/development/limit_ee_conflicts.md b/doc/development/limit_ee_conflicts.md index 568dedf1669bf9ae39682da61c9c637a592cf56b..2d82b09f30124f12579180ffef488e0daf68a8b0 100644 --- a/doc/development/limit_ee_conflicts.md +++ b/doc/development/limit_ee_conflicts.md @@ -2,19 +2,26 @@ This guide contains best-practices for avoiding conflicts between CE and EE. -## Context +## Daily CE Upstream merge -Usually, GitLab Community Edition is merged into the Enterprise Edition once a -week. During these merges, it's very common to get conflicts when some changes -in CE do not apply cleanly to EE. +GitLab Community Edition is merged daily into the Enterprise Edition (look for +the [`CE Upstream` merge requests]). The daily merge is currently done manually +by four individuals. -There are a few things that can help you as a developer to: +**If a developer pings you in a `CE Upstream` merge request for help with +resolving conflicts, please help them because it means that you didn't do your +job to reduce the conflicts nor to ease their resolution in the first place!** -- know when your merge request to CE will conflict when merged to EE -- avoid such conflicts in the first place -- ease future conflict resolutions if conflict is inevitable +To avoid the conflicts beforehand when working on CE, there are a few tools and +techniques that can help you: -## Check the `rake ee_compat_check` in your merge requests +- know what are the usual types of conflicts and how to prevent them +- the CI `rake ee_compat_check` job tells you if you need to open an EE-version + of your CE merge request + +[`CE Upstream` merge requests]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests?label_name%5B%5D=CE+upstream + +## Check the status of the CI `rake ee_compat_check` job For each commit (except on `master`), the `rake ee_compat_check` CI job tries to detect if the current branch's changes will conflict during the CE->EE merge. diff --git a/doc/development/testing.md b/doc/development/testing.md index 761847b2bab380515b51a19137f5d83e1515583c..500cbefcffbbdf5415e05e01936c7d41928cece0 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -115,6 +115,10 @@ Here are some things to keep in mind regarding test performance: ### Features / Integration +GitLab uses [rspec-rails feature specs] to test features in a browser +environment. These are [capybara] specs running on the headless [poltergeist] +driver. + - Feature specs live in `spec/features/` and should be named `ROLE_ACTION_spec.rb`, such as `user_changes_password_spec.rb`. - Use only one `feature` block per feature spec file. @@ -122,6 +126,10 @@ Here are some things to keep in mind regarding test performance: - Avoid scenario titles that add no information, such as "successfully." - Avoid scenario titles that repeat the feature title. +[rspec-rails feature specs]: https://github.com/rspec/rspec-rails#feature-specs +[capybara]: https://github.com/teamcapybara/capybara +[poltergeist]: https://github.com/teampoltergeist/poltergeist + ## Spinach (feature) tests GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426) diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md index 205b3e8a8359a08bb727acccf4152452c8c669a0..ead79ba6a10c6f9766323b2f8468ea50d5799f4b 100644 --- a/doc/development/ux_guide/copy.md +++ b/doc/development/ux_guide/copy.md @@ -176,4 +176,4 @@ Portions of this page are modifications based on work created and shared by the [products]: https://about.gitlab.com/products/ "GitLab products page" [serial comma]: https://en.wikipedia.org/wiki/Serial_comma "“Serial comma” in Wikipedia" [android project]: http://source.android.com/ -[creative commons]: http://creativecommons.org/licenses/by/2.5/ \ No newline at end of file +[creative commons]: http://creativecommons.org/licenses/by/2.5/ diff --git a/doc/install/installation.md b/doc/install/installation.md index 0f07085942a0b85ad94840fe1a6676c49239ed0a..5ba338ba7d1d92d92db677a3a68be5cbb849eb95 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -39,6 +39,7 @@ The GitLab installation consists of setting up the following components: 1. Packages / Dependencies 1. Ruby 1. Go +1. Node 1. System Users 1. Database 1. Redis @@ -63,7 +64,7 @@ up-to-date and install it. Install the required packages (needed to compile Ruby and native extensions to Ruby gems): - sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake nodejs + sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake If you want to use Kerberos for user authentication, then install libkrb5-dev: @@ -151,13 +152,30 @@ page](https://golang.org/dl). sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ rm go1.5.3.linux-amd64.tar.gz -## 4. System Users +## 4. Node + +Since GitLab 8.17, GitLab requires the use of node >= v4.3.0 to compile +javascript assets, and starting in GitLab 9.0, yarn >= v0.17.0 is required to +manage javascript dependencies. In many distros the versions provided by the +official package repositories are out of date, so we'll need to install through +the following commands: + + # install node v7.x + curl --location https://deb.nodesource.com/setup_7.x | bash - + sudo apt-get install -y nodejs + + # install yarn + curl --location https://yarnpkg.com/install.sh | bash - + +Visit the official websites for [node](https://nodejs.org/en/download/package-manager/) and [yarn](https://yarnpkg.com/en/docs/install/) if you have any trouble with these steps. + +## 5. System Users Create a `git` user for GitLab: sudo adduser --disabled-login --gecos 'GitLab' git -## 5. Database +## 6. Database We recommend using a PostgreSQL database. For MySQL check the [MySQL setup guide](database_mysql.md). @@ -218,7 +236,7 @@ We recommend using a PostgreSQL database. For MySQL check the gitlabhq_production> \q ``` -## 6. Redis +## 7. Redis GitLab requires at least Redis 2.8. @@ -263,7 +281,7 @@ sudo service redis-server restart sudo usermod -aG redis git ``` -## 7. GitLab +## 8. GitLab # We'll install GitLab into home directory of the user "git" cd /home/git @@ -451,7 +469,8 @@ Check if GitLab and its environment are configured correctly: ### Compile Assets - sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production + sudo -u git -H yarn install --production --pure-lockfile + sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production NODE_ENV=production ### Start Your GitLab Instance @@ -459,7 +478,7 @@ Check if GitLab and its environment are configured correctly: # or sudo /etc/init.d/gitlab restart -## 8. Nginx +## 9. Nginx **Note:** Nginx is the officially supported web server for GitLab. If you cannot or do not want to use Nginx as your web server, have a look at the [GitLab recipes](https://gitlab.com/gitlab-org/gitlab-recipes/). diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index 30f0c15daccf9dec7b9e7000455a7d38014f9520..242890af98141738bf5a2fe813a59a84bfd9506e 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -1,3 +1 @@ -# GitLab LDAP integration - -This document was moved under [`administration/auth/ldap`](../administration/auth/ldap.md). +This document was moved to [`administration/auth/ldap`](../administration/auth/ldap.md). diff --git a/doc/profile/preferences.md b/doc/profile/preferences.md index 073b87975081e559e67810205c7e32a8dbb2cafb..4f2b00f3dd11383dd088e5cc7bf09a4339f95771 100644 --- a/doc/profile/preferences.md +++ b/doc/profile/preferences.md @@ -3,13 +3,6 @@ Settings in the **Profile > Preferences** page allow the user to customize various aspects of the site to their liking. -## Application theme - -Changing this setting allows the user to customize the color scheme used for the -navigation bar on the left side of the screen. - -The default is **Charcoal**. - ## Syntax highlighting theme _GitLab uses the [rouge ruby library][rouge] for syntax highlighting. For a diff --git a/doc/update/8.16-to-8.17.md b/doc/update/8.16-to-8.17.md index 53c2bc560e8203f69fc7c31e3df5341eaa3d5afb..954109ba18f148b3f270116c280142994b1b01d0 100644 --- a/doc/update/8.16-to-8.17.md +++ b/doc/update/8.16-to-8.17.md @@ -49,7 +49,19 @@ Install Bundler: sudo gem install bundler --no-ri --no-rdoc ``` -### 4. Get latest code +### 4. Update Node + +GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and +it has a minimum requirement of node v4.3.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v4.3.0` you will need to update to a newer version. You +can find instructions to install from community maintained packages or compile +from source at the nodejs.org website. + +<https://nodejs.org/en/download/> + +### 5. Get latest code ```bash cd /home/git/gitlab @@ -76,7 +88,7 @@ cd /home/git/gitlab sudo -u git -H git checkout 8-17-stable-ee ``` -### 5. Install libs, migrations, etc. +### 6. Install libs, migrations, etc. ```bash cd /home/git/gitlab @@ -93,13 +105,16 @@ sudo -u git -H bundle clean # Run database migrations sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production +# Install/update frontend asset dependencies +sudo -u git -H npm install --production + # Clean up assets and cache -sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production +sudo -u git -H bundle exec rake gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production ``` **MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). -### 6. Update gitlab-workhorse +### 7. Update gitlab-workhorse Install and compile gitlab-workhorse. This requires [Go 1.5](https://golang.org/dl) which should already be on your system from @@ -111,7 +126,7 @@ cd /home/git/gitlab sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production ``` -### 7. Update gitlab-shell +### 8. Update gitlab-shell ```bash cd /home/git/gitlab-shell @@ -120,7 +135,7 @@ sudo -u git -H git fetch --all --tags sudo -u git -H git checkout v4.1.1 ``` -### 8. Update configuration files +### 9. Update configuration files #### New configuration options for `gitlab.yml` @@ -194,14 +209,14 @@ For Ubuntu 16.04.1 LTS: sudo systemctl daemon-reload ``` -### 9. Start application +### 10. Start application ```bash sudo service gitlab start sudo service nginx restart ``` -### 10. Check application status +### 11. Check application status Check if GitLab and its environment are configured correctly: diff --git a/doc/user/project/integrations/img/services_templates_redmine_example.png b/doc/user/project/integrations/img/services_templates_redmine_example.png index 50d20510dafd0dbfcfd89f16abc21002d87a79d4..379cef9888d91c2b27d0f285a0171ef3bc15a13e 100644 Binary files a/doc/user/project/integrations/img/services_templates_redmine_example.png and b/doc/user/project/integrations/img/services_templates_redmine_example.png differ diff --git a/doc/user/project/integrations/services_templates.md b/doc/user/project/integrations/services_templates.md index be6d13b6d2b8fb58ccc801cf3470deb52ef6ea64..5b04d7d88b8c90dd28427a8d16a131224f924f5b 100644 --- a/doc/user/project/integrations/services_templates.md +++ b/doc/user/project/integrations/services_templates.md @@ -1,25 +1,26 @@ -# Services Templates +# Services templates A GitLab administrator can add a service template that sets a default for each -project. This makes it much easier to configure individual projects. +project. After a service template is enabled, it will be applied to new +projects only and its details will be pre-filled on the project's Service page. -After the template is created, the template details will be pre-filled on a -project's Service page. - -## Enable a Service template +## Enable a service template In GitLab's Admin area, navigate to **Service Templates** and choose the service template you wish to create. -For example, in the image below you can see Redmine. +## Services for external issue trackers + +In the image below you can see how a service template for Redmine would look +like.  --- -**NOTE:** For each project, you will still need to configure the issue tracking +For each project, you will still need to configure the issue tracking URLs by replacing `:issues_tracker_id` in the above screenshot with the ID used by your external issue tracker. Prior to GitLab v7.8, this ID was configured in the project settings, and GitLab would automatically update the URL configured in `gitlab.yml`. This behavior is now deprecated and all issue tracker URLs -must be configured directly within the project's **Services** settings. +must be configured directly within the project's **Integrations** settings. diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index 80cdb49a1d3638ede9b35168fbda9c2032da0b86..c398ac2eb252c559c4e07088347ecb1fd8510a4c 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -62,9 +62,9 @@ pipelines** checkbox and save the changes. ## Badges -In the pipelines settings page you can find job status and test coverage +In the pipelines settings page you can find pipeline status and test coverage badges for your project. The latest successful pipeline will be used to read -the job status and test coverage values. +the pipeline status and test coverage values. Visit the pipelines settings page in your project to see the exact link to your badges, as well as ways to embed the badge image in your HTML or Markdown @@ -72,7 +72,7 @@ pages.  -### Job status badge +### Pipeline status badge Depending on the status of your job, a badge can have the following values: @@ -82,7 +82,7 @@ Depending on the status of your job, a badge can have the following values: - skipped - unknown -You can access a job status badge image using the following link: +You can access a pipeline status badge image using the following link: ``` https://example.gitlab.com/<namespace>/<project>/badges/<branch>/build.svg diff --git a/doc/user/snippets.md b/doc/user/snippets.md new file mode 100644 index 0000000000000000000000000000000000000000..417360e08ac2d23a8bfa19053920c991b32571c4 --- /dev/null +++ b/doc/user/snippets.md @@ -0,0 +1,19 @@ +# Snippets + +Snippets are little bits of code or text. + +There are 2 types of snippets - project snippets and personal snippets. + +## Project snippets + +Project snippets are always related to a specific project - see [Project features](../workflow/project_features.md) for more information. + +## Personal snippets + +Personal snippets are not related to any project and can be created completely independently. There are 3 visibility levels that can be set (public, internal, private - see [Public Access](../public_access/public_access.md) for more information). + +## Downloading snippets + +You can download the raw content of a snippet. + +By default snippets will be downloaded with Linux-style line endings (`LF`). If you want to preserve the original line endings you need to add a parameter `line_ending=raw` (eg. `https://gitlab.com/snippets/SNIPPET_ID/raw?line_ending=raw`). In case a snippet was created using the GitLab web interface the original line ending is Windows-like (`CRLF`). diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 7a97b87f1c5493ddcb67bbaad5ebc3d6a0290b4a..9e7ee47387ce51b918b943ba59fcd034bdce31fb 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -39,3 +39,4 @@ - [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md) - [Importing from SVN, GitHub, Bitbucket, etc](importing/README.md) - [Todos](todos.md) +- [Snippets](../user/snippets.md) diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index c228ea72f22731561a0227a985351c3a3a4ccd61..4889e3ec50c279d72dfce3d6e3d82a7de44d01a8 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -67,7 +67,7 @@ With GitLab flow we offer additional guidance for these questions.  GitHub flow does assume you are able to deploy to production every time you merge a feature branch. -This is possible for SaaS applications but are many cases where this is not possible. +This is possible for SaaS applications but there are many cases where this is not possible. One would be a situation where you are not in control of the exact release moment, for example an iOS application that needs to pass App Store validation. Another example is when you have deployment windows (workdays from 10am to 4pm when the operations team is at full capacity) but you also merge code at other times. In these cases you can make a production branch that reflects the deployed code. diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md index 99d7c18f072c21671f72e29339ed4491570b4bc9..6eb457fde3f999048ebc7496f68d52c2c15c46fd 100644 --- a/doc/workflow/todos.md +++ b/doc/workflow/todos.md @@ -32,6 +32,29 @@ A Todo appears in your Todos dashboard when: >**Note:** Commenting on a commit will _not_ trigger a Todo. +### Directly addressed Todos + +> [Introduced][ce-7926] in GitLab 9.0. + +If you are mentioned at the start of a line, the todo you receive will be listed +as 'directly addressed'. For instance, in this comment: + +```markdown +@alice What do you think? cc: @bob + +- @carol can you please have a look? + +>>> +@dan what do you think? +>>> + +@erin @frank thank you! +``` + +The people receiving directly addressed todos are `@alice`, `@erin`, and +`@frank`. Directly addressed todos only differ from mention todos in their type, +for filtering; otherwise, they appear as normal. + ### Manually creating a Todo You can also add an issue or merge request to your Todos dashboard by clicking @@ -85,8 +108,9 @@ There are four kinds of filters you can use on your Todos dashboard. | Project | Filter by project | | Author | Filter by the author that triggered the Todo | | Type | Filter by issue or merge request | -| Action | Filter by the action that triggered the Todo (Assigned or Mentioned)| +| Action | Filter by the action that triggered the Todo | You can also filter by more than one of these at the same time. [ce-2817]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2817 +[ce-7926]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7926 diff --git a/features/dashboard/issues.feature b/features/dashboard/issues.feature deleted file mode 100644 index 99dad88a40289274f1c4fd5da5d8066f8d6e002a..0000000000000000000000000000000000000000 --- a/features/dashboard/issues.feature +++ /dev/null @@ -1,21 +0,0 @@ -@dashboard -Feature: Dashboard Issues - Background: - Given I sign in as a user - And I have authored issues - And I have assigned issues - And I have other issues - And I visit dashboard issues page - - Scenario: I should see assigned issues - Then I should see issues assigned to me - - @javascript - Scenario: I should see authored issues - When I click "Authored by me" link - Then I should see issues authored by me - - @javascript - Scenario: I should see all issues - When I click "All" link - Then I should see all issues diff --git a/features/project/labels.feature b/features/project/labels.feature deleted file mode 100644 index 955bc3d8b1b0e03b3efd1c501aaa7d0c803a7215..0000000000000000000000000000000000000000 --- a/features/project/labels.feature +++ /dev/null @@ -1,15 +0,0 @@ -@labels -Feature: Labels - Background: - Given I sign in as a user - And I own project "Shop" - And project "Shop" has labels: "bug", "feature", "enhancement" - When I visit project "Shop" labels page - - @javascript - Scenario: I can subscribe to a label - Then I should see that I am not subscribed to the "bug" label - When I click button "Subscribe" for the "bug" label - Then I should see that I am subscribed to the "bug" label - When I click button "Unsubscribe" for the "bug" label - Then I should see that I am not subscribed to the "bug" label diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb deleted file mode 100644 index 4e15d79ae742ae5221a3cdd6f3f4b27d104cb746..0000000000000000000000000000000000000000 --- a/features/steps/dashboard/issues.rb +++ /dev/null @@ -1,91 +0,0 @@ -class Spinach::Features::DashboardIssues < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include Select2Helper - - step 'I should see issues assigned to me' do - should_see(assigned_issue) - should_not_see(authored_issue) - should_not_see(other_issue) - end - - step 'I should see issues authored by me' do - should_see(authored_issue) - should_see(authored_issue_on_public_project) - should_not_see(assigned_issue) - should_not_see(other_issue) - end - - step 'I should see all issues' do - should_see(authored_issue) - should_see(assigned_issue) - should_see(other_issue) - end - - step 'I have authored issues' do - authored_issue - authored_issue_on_public_project - end - - step 'I have assigned issues' do - assigned_issue - end - - step 'I have other issues' do - other_issue - end - - step 'I click "Authored by me" link' do - find("#assignee_id").set("") - find(".js-author-search", match: :first).click - find(".dropdown-menu-author li a", match: :first, text: current_user.to_reference).click - end - - step 'I click "All" link' do - find(".js-author-search").click - expect(page).to have_selector(".dropdown-menu-author li a") - find(".dropdown-menu-author li a", match: :first).click - expect(page).not_to have_selector(".dropdown-menu-author li a") - - find(".js-assignee-search").click - expect(page).to have_selector(".dropdown-menu-assignee li a") - find(".dropdown-menu-assignee li a", match: :first).click - expect(page).not_to have_selector(".dropdown-menu-assignee li a") - end - - def should_see(issue) - expect(page).to have_content(issue.title[0..10]) - end - - def should_not_see(issue) - expect(page).not_to have_content(issue.title[0..10]) - end - - def assigned_issue - @assigned_issue ||= create :issue, assignee: current_user, project: project - end - - def authored_issue - @authored_issue ||= create :issue, author: current_user, project: project - end - - def other_issue - @other_issue ||= create :issue, project: project - end - - def authored_issue_on_public_project - @authored_issue_on_public_project ||= create :issue, author: current_user, project: public_project - end - - def project - @project ||= begin - project = create(:empty_project) - project.team << [current_user, :master] - project - end - end - - def public_project - @public_project ||= create(:empty_project, :public) - end -end diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb index 2bbc43b491f9fa784ed425509424ef9a7160acfe..eb906a55a831a20125797e06fa5ec0f89c4aedd6 100644 --- a/features/steps/dashboard/todos.rb +++ b/features/steps/dashboard/todos.rb @@ -47,7 +47,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps page.within('.todos-pending-count') { expect(page).to have_content '3' } expect(page).to have_content 'To do 3' expect(page).to have_content 'Done 1' - should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference(full: true)}" + should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, state: :done_reversible) end step 'I mark all todos as done' do @@ -71,7 +71,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps click_link 'Done 1' expect(page).to have_link project.name_with_namespace - should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, false) + should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, state: :done_irreversible) end step 'I should see all todos marked as done' do @@ -81,10 +81,10 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps click_link 'Done 4' expect(page).to have_link project.name_with_namespace - should_see_todo(1, "John Doe assigned you merge request #{merge_request_reference}", merge_request.title, false) - should_see_todo(2, "John Doe mentioned you on issue #{issue_reference}", "#{current_user.to_reference} Wdyt?", false) - should_see_todo(3, "John Doe assigned you issue #{issue_reference}", issue.title, false) - should_see_todo(4, "Mary Jane mentioned you on issue #{issue_reference}", issue.title, false) + should_see_todo(1, "John Doe assigned you merge request #{merge_request_reference}", merge_request.title, state: :done_irreversible) + should_see_todo(2, "John Doe mentioned you on issue #{issue_reference}", "#{current_user.to_reference} Wdyt?", state: :done_irreversible) + should_see_todo(3, "John Doe assigned you issue #{issue_reference}", issue.title, state: :done_irreversible) + should_see_todo(4, "Mary Jane mentioned you on issue #{issue_reference}", issue.title, state: :done_irreversible) end step 'I filter by "Enterprise"' do @@ -140,15 +140,20 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps page.should have_css('.identifier', text: 'Merge Request !1') end - def should_see_todo(position, title, body, pending = true) + def should_see_todo(position, title, body, state: :pending) page.within(".todo:nth-child(#{position})") do expect(page).to have_content title expect(page).to have_content body - if pending + if state == :pending expect(page).to have_link 'Done' - else + elsif state == :done_reversible + expect(page).to have_link 'Undo' + elsif state == :done_irreversible + expect(page).not_to have_link 'Undo' expect(page).not_to have_link 'Done' + else + raise 'Invalid state given, valid states: :pending, :done_reversible, :done_irreversible' end end end diff --git a/features/steps/project/labels.rb b/features/steps/project/labels.rb deleted file mode 100644 index dbeb07c78db7980f93acf56fedb41380f3ea4b07..0000000000000000000000000000000000000000 --- a/features/steps/project/labels.rb +++ /dev/null @@ -1,32 +0,0 @@ -class Spinach::Features::Labels < Spinach::FeatureSteps - include SharedAuthentication - include SharedIssuable - include SharedProject - include SharedPaths - - step 'And I visit project "Shop" labels page' do - visit namespace_project_labels_path(project.namespace, project) - end - - step 'I should see that I am subscribed to the "bug" label' do - expect(subscribe_button).to have_content 'Unsubscribe' - end - - step 'I should see that I am not subscribed to the "bug" label' do - expect(subscribe_button).to have_content 'Subscribe' - end - - step 'I click button "Unsubscribe" for the "bug" label' do - subscribe_button.click - end - - step 'I click button "Subscribe" for the "bug" label' do - subscribe_button.click - end - - private - - def subscribe_button - first('.js-subscribe-button', visible: true) - end -end diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb index d008a8a26af61cf01d2f469b045d8962480e644f..5bc3a1f5ac44195cd6d7a0aee5b7c9a70efb5512 100644 --- a/features/steps/shared/builds.rb +++ b/features/steps/shared/builds.rb @@ -11,7 +11,7 @@ module SharedBuilds step 'project has a recent build' do @pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master') - @build = create(:ci_build_with_coverage, pipeline: @pipeline) + @build = create(:ci_build, :coverage, pipeline: @pipeline) end step 'recent build is successful' do diff --git a/lib/api/api.rb b/lib/api/api.rb index 06346ae822a1f22c29fad8013ea223172f1bbf7a..dbb7271ccbd24a6046ecda8b2edead478bc7df49 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -5,13 +5,22 @@ module API version %w(v3 v4), using: :path version 'v3', using: :path do + mount ::API::V3::Boards + mount ::API::V3::Branches mount ::API::V3::DeployKeys mount ::API::V3::Issues + mount ::API::V3::Labels mount ::API::V3::Members + mount ::API::V3::MergeRequestDiffs mount ::API::V3::MergeRequests + mount ::API::V3::ProjectHooks mount ::API::V3::Projects mount ::API::V3::ProjectSnippets + mount ::API::V3::Repositories + mount ::API::V3::SystemHooks + mount ::API::V3::Tags mount ::API::V3::Templates + mount ::API::V3::Users end before { allow_access_with_scope :api } diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb index 58a4df54bea7541498cc30d3b22cec618ec69cae..2ef327217ea7929796bc6e395aa0c29dfe859a6d 100644 --- a/lib/api/award_emoji.rb +++ b/lib/api/award_emoji.rb @@ -28,8 +28,8 @@ module API end get endpoint do if can_read_awardable? - awards = paginate(awardable.award_emoji) - present awards, with: Entities::AwardEmoji + awards = awardable.award_emoji + present paginate(awards), with: Entities::AwardEmoji else not_found!("Award Emoji") end diff --git a/lib/api/boards.rb b/lib/api/boards.rb index 13752eb49476c8ad6842a71d4fa936e3a72bff7e..f4226e5a89d759235177a299a195957072d73df0 100644 --- a/lib/api/boards.rb +++ b/lib/api/boards.rb @@ -1,6 +1,7 @@ module API - # Boards API class Boards < Grape::API + include PaginationParams + before { authenticate! } params do @@ -11,9 +12,12 @@ module API detail 'This feature was introduced in 8.13' success Entities::Board end + params do + use :pagination + end get ':id/boards' do authorize!(:read_board, user_project) - present user_project.boards, with: Entities::Board + present paginate(user_project.boards), with: Entities::Board end params do @@ -40,9 +44,12 @@ module API detail 'Does not include `done` list. This feature was introduced in 8.13' success Entities::List end + params do + use :pagination + end get '/lists' do authorize!(:read_board, user_project) - present board_lists, with: Entities::List + present paginate(board_lists), with: Entities::List end desc 'Get a list of a project board' do diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 9331be1f7de1b46d545fab132ff78e397db12004..9d1f5a28ef62f76c84a77362320027949462de53 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -1,8 +1,9 @@ require 'mime/types' module API - # Projects API class Branches < Grape::API + include PaginationParams + before { authenticate! } before { authorize! :download_code, user_project } @@ -13,10 +14,13 @@ module API desc 'Get a project repository branches' do success Entities::RepoBranch end + params do + use :pagination + end get ":id/repository/branches" do - branches = user_project.repository.branches.sort_by(&:name) + branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name)) - present branches, with: Entities::RepoBranch, project: user_project + present paginate(branches), with: Entities::RepoBranch, project: user_project end desc 'Get a single branch' do diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 2fefe760d24c5b6c68786c3c9b85feb4b578bb4e..3b314c89c6e90b6119f239ab05ab4e04fe023e8a 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -16,16 +16,13 @@ module API end params do optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' - optional :since, type: String, desc: 'Only commits after or in this date will be returned' - optional :until, type: String, desc: 'Only commits before or in this date will be returned' + optional :since, type: DateTime, desc: 'Only commits after or on this date will be returned' + optional :until, type: DateTime, desc: 'Only commits before or on this date will be returned' optional :page, type: Integer, default: 0, desc: 'The page for pagination' optional :per_page, type: Integer, default: 20, desc: 'The number of results per page' optional :path, type: String, desc: 'The file path' end get ":id/repository/commits" do - # TODO remove the next line for 9.0, use DateTime type in the params block - datetime_attributes! :since, :until - ref = params[:ref_name] || user_project.try(:default_branch) || 'master' offset = params[:page] * params[:per_page] @@ -114,7 +111,7 @@ module API commit = user_project.commit(params[:sha]) not_found! 'Commit' unless commit - notes = Note.where(commit_id: commit.id).order(:created_at) + notes = user_project.notes.where(commit_id: commit.id).order(:created_at) present paginate(notes), with: Entities::CommitNote end diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 3f5183d46a2e39c8946e8de22a0f382b794058f6..69e85c27a65d6063daa595f753084472b9aa731d 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -1,12 +1,17 @@ module API class DeployKeys < Grape::API + include PaginationParams + before { authenticate! } + desc 'Return all deploy keys' + params do + use :pagination + end get "deploy_keys" do authenticated_as_admin! - keys = DeployKey.all - present keys, with: Entities::SSHKey + present paginate(DeployKey.all), with: Entities::SSHKey end params do @@ -18,8 +23,11 @@ module API desc "Get a specific project's deploy keys" do success Entities::SSHKey end + params do + use :pagination + end get ":id/deploy_keys" do - present user_project.deploy_keys, with: Entities::SSHKey + present paginate(user_project.deploy_keys), with: Entities::SSHKey end desc 'Get single deploy key' do @@ -85,20 +93,6 @@ module API end end - desc 'Disable a deploy key for a project' do - detail 'This feature was added in GitLab 8.11' - success Entities::SSHKey - end - params do - requires :key_id, type: Integer, desc: 'The ID of the deploy key' - end - delete ":id/deploy_keys/:key_id/disable" do - key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) - key.destroy - - present key.deploy_key, with: Entities::SSHKey - end - desc 'Delete deploy key for a project' do success Key end @@ -107,11 +101,9 @@ module API end delete ":id/deploy_keys/:key_id" do key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) - if key - key.destroy - else - not_found!('Deploy Key') - end + not_found!('Deploy Key') unless key + + key.destroy end end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 94f5bd510c9430e6ecf35f52c8421c5e164eb874..ff27856b8003e1a101547a6f6c4c331eea83d584 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -26,7 +26,7 @@ module API expose :last_sign_in_at expose :confirmed_at expose :email - expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at + expose :color_scheme_id, :projects_limit, :current_sign_in_at expose :identities, using: Entities::Identity expose :can_create_group?, as: :can_create_group expose :can_create_project?, as: :can_create_project diff --git a/lib/api/files.rb b/lib/api/files.rb index 2ecdd747c8e598dbe9c6759e82e09cb3d8bbf787..6e16ccd2fd8c86b95f682f1c60ee2bc08c0d84df 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -1,5 +1,4 @@ module API - # Projects API class Files < Grape::API helpers do def commit_params(attrs) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 13896dd91b99993d2882d7be8dfcbaf50cbc6ac4..7b6fae866eb16a35939fcad72d4a5b3c7138767a 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -160,22 +160,6 @@ module API ActionController::Parameters.new(attrs).permit! end - # Checks the occurrences of datetime attributes, each attribute if present in the params hash must be in ISO 8601 - # format (YYYY-MM-DDTHH:MM:SSZ) or a Bad Request error is invoked. - # - # Parameters: - # keys (required) - An array consisting of elements that must be parseable as dates from the params hash - def datetime_attributes!(*keys) - keys.each do |key| - begin - params[key] = Time.xmlschema(params[key]) if params[key].present? - rescue ArgumentError - message = "\"" + key.to_s + "\" must be a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ" - render_api_error!(message, 400) - end - end - end - def filter_by_iid(items, iid) items.where(iid: iid) end diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb index 2199eea7e5ffa40b78c86f8aeee35555de4c24b5..0764b58fb4cea5c53f85dc0db4d35854a6a6fe2c 100644 --- a/lib/api/helpers/pagination.rb +++ b/lib/api/helpers/pagination.rb @@ -2,7 +2,7 @@ module API module Helpers module Pagination def paginate(relation) - relation.page(params[:page]).per(params[:per_page].to_i).tap do |data| + relation.page(params[:page]).per(params[:per_page]).tap do |data| add_pagination_headers(data) end end diff --git a/lib/api/labels.rb b/lib/api/labels.rb index 652786d4e3eba436d881c3d1346138be8ef708ec..d2955af3f95e322beb588e5640d9190d2ceeb1ca 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -1,6 +1,7 @@ module API - # Labels API class Labels < Grape::API + include PaginationParams + before { authenticate! } params do @@ -10,8 +11,11 @@ module API desc 'Get all labels of the project' do success Entities::Label end + params do + use :pagination + end get ':id/labels' do - present available_labels, with: Entities::Label, current_user: current_user, project: user_project + present paginate(available_labels), with: Entities::Label, current_user: current_user, project: user_project end desc 'Create a new label' do diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb index bc3d69f6904b6de2cbd08f20655f15c1cf2d40b7..4901a7cfea62d9bc5207b1a9908f4344f024fd46 100644 --- a/lib/api/merge_request_diffs.rb +++ b/lib/api/merge_request_diffs.rb @@ -1,6 +1,8 @@ module API # MergeRequestDiff API class MergeRequestDiffs < Grape::API + include PaginationParams + before { authenticate! } resource :projects do @@ -12,12 +14,12 @@ module API params do requires :id, type: String, desc: 'The ID of a project' requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + use :pagination end - get ":id/merge_requests/:merge_request_id/versions" do merge_request = find_merge_request_with_access(params[:merge_request_id]) - present merge_request.merge_request_diffs, with: Entities::MergeRequestDiff + present paginate(merge_request.merge_request_diffs), with: Entities::MergeRequestDiff end desc 'Get a single merge request diff version' do diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 8e09a6f73549418186c4e3b170dfba8427a286c7..bdd764abfebe50898dfd7fd73af02c6249a88ebc 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -119,8 +119,9 @@ module API end get ':id/merge_requests/:merge_request_id/commits' do merge_request = find_merge_request_with_access(params[:merge_request_id]) + commits = ::Kaminari.paginate_array(merge_request.commits) - present merge_request.commits, with: Entities::RepoCommit + present paginate(commits), with: Entities::RepoCommit end desc 'Show the merge request changes' do diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index 3c373a84ec528614152a0bd04ff091077234e430..0b4ed76b35cab7a349a03778bc79600f35c0fd64 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -120,6 +120,28 @@ module API issues = IssuesFinder.new(current_user, finder_params).execute present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project end + + desc 'Get all merge requests for a single project milestone' do + detail 'This feature was introduced in GitLab 9.' + success Entities::MergeRequest + end + params do + requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' + use :pagination + end + get ':id/milestones/:milestone_id/merge_requests' do + authorize! :read_milestone, user_project + + milestone = user_project.milestones.find(params[:milestone_id]) + + finder_params = { + project_id: user_project.id, + milestone_id: milestone.id + } + + merge_requests = MergeRequestsFinder.new(current_user, finder_params).execute + present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user, project: user_project + end end end end diff --git a/lib/api/pagination_params.rb b/lib/api/pagination_params.rb index 8c1e4381a742402df0606202e0c9637a7b5e60d0..f566eb3ed2b12aaa88d31f859a3626f4cafcc454 100644 --- a/lib/api/pagination_params.rb +++ b/lib/api/pagination_params.rb @@ -15,8 +15,8 @@ module API included do helpers do params :pagination do - optional :page, type: Integer, desc: 'Current page number' - optional :per_page, type: Integer, desc: 'Number of items per page' + optional :page, type: Integer, default: 1, desc: 'Current page number' + optional :per_page, type: Integer, default: 20, desc: 'Number of items per page' end end end diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index cb679e6658a3085a7e61f53fd8555a3f56ae3cc6..f7a28d7ad10284d3dc6fd85bc66ef2038a3f09f0 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -32,9 +32,7 @@ module API use :pagination end get ":id/hooks" do - hooks = paginate user_project.hooks - - present hooks, with: Entities::ProjectHook + present paginate(user_project.hooks), with: Entities::ProjectHook end desc 'Get a project hook' do diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 68c2732ec802c6ba86a8e4bfd6f66f01c1744443..366e5679eddb3eae08d39ac87604b53c54621d6b 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -266,7 +266,7 @@ module API desc 'Unstar a project' do success Entities::Project end - delete ':id/star' do + post ':id/unstar' do if current_user.starred?(user_project) current_user.toggle_star(user_project) user_project.reload diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 4ca6646a6f1e99516bb8dab8e0ea5bd38e187203..bfda6f45b0a8d5697bab88b3d37d1a909aa901ce 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -2,6 +2,8 @@ require 'mime/types' module API class Repositories < Grape::API + include PaginationParams + before { authorize! :download_code, user_project } params do @@ -24,6 +26,7 @@ module API optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' optional :path, type: String, desc: 'The path of the tree' optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree' + use :pagination end get ':id/repository/tree' do ref = params[:ref_name] || user_project.try(:default_branch) || 'master' @@ -33,8 +36,8 @@ module API not_found!('Tree') unless commit tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive]) - - present tree.sorted_entries, with: Entities::RepoTreeObject + entries = ::Kaminari.paginate_array(tree.sorted_entries) + present paginate(entries), with: Entities::RepoTreeObject end desc 'Get a raw file contents' @@ -100,10 +103,13 @@ module API desc 'Get repository contributors' do success Entities::Contributor end + params do + use :pagination + end get ':id/repository/contributors' do begin - present user_project.repository.contributors, - with: Entities::Contributor + contributors = ::Kaminari.paginate_array(user_project.repository.contributors) + present paginate(contributors), with: Entities::Contributor rescue not_found! end diff --git a/lib/api/runners.rb b/lib/api/runners.rb index 4816b5ed1b782d2aafaaf05eae1b634352c45874..4fbd40965335740718b7ac352b879e6456f81cea 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -60,8 +60,9 @@ module API put ':id' do runner = get_runner(params.delete(:id)) authenticate_update_runner!(runner) + update_service = Ci::UpdateRunnerService.new(runner) - if runner.update(declared_params(include_missing: false)) + if update_service.update(declared_params(include_missing: false)) present runner, with: Entities::RunnerDetails, current_user: current_user else render_validation_error!(runner) diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb index 708ec8cfe70fccc626dfd1dd685049f74b61051e..d038a3fa828321257e31f7d654adeaf4a5cabc78 100644 --- a/lib/api/system_hooks.rb +++ b/lib/api/system_hooks.rb @@ -1,6 +1,7 @@ module API - # Hooks API class SystemHooks < Grape::API + include PaginationParams + before do authenticate! authenticated_as_admin! @@ -10,10 +11,11 @@ module API desc 'Get the list of system hooks' do success Entities::Hook end + params do + use :pagination + end get do - hooks = SystemHook.all - - present hooks, with: Entities::Hook + present paginate(SystemHook.all), with: Entities::Hook end desc 'Create a new system hook' do diff --git a/lib/api/tags.rb b/lib/api/tags.rb index b6fd8f569a91554c8c31ee6551fbefac136a5365..86759ab882f72f730333d0a7029d4fe5968c60c4 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -1,6 +1,7 @@ module API - # Git Tags API class Tags < Grape::API + include PaginationParams + before { authorize! :download_code, user_project } params do @@ -10,9 +11,12 @@ module API desc 'Get a project repository tags' do success Entities::RepoTag end + params do + use :pagination + end get ":id/repository/tags" do - present user_project.repository.tags.sort_by(&:name).reverse, - with: Entities::RepoTag, project: user_project + tags = ::Kaminari.paginate_array(user_project.repository.tags.sort_by(&:name).reverse) + present paginate(tags), with: Entities::RepoTag, project: user_project end desc 'Get a single repository tag' do diff --git a/lib/api/templates.rb b/lib/api/templates.rb index 8a2d66efd892ade36ec127160bfe4417b96f66a5..0fc13b35d5bddd6f4608e805724f101a3ea4250b 100644 --- a/lib/api/templates.rb +++ b/lib/api/templates.rb @@ -1,5 +1,7 @@ module API class Templates < Grape::API + include PaginationParams + GLOBAL_TEMPLATE_TYPES = { gitignores: { klass: Gitlab::Template::GitignoreTemplate, @@ -51,12 +53,14 @@ module API end params do optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses' + use :pagination end get "templates/licenses" do options = { featured: declared(params).popular.present? ? true : nil } - present Licensee::License.all(options), with: ::API::Entities::RepoLicense + licences = ::Kaminari.paginate_array(Licensee::License.all(options)) + present paginate(licences), with: Entities::RepoLicense end desc 'Get the text for a specific license' do @@ -82,8 +86,12 @@ module API detail "This feature was introduced in GitLab #{gitlab_version}." success Entities::TemplatesList end + params do + use :pagination + end get "templates/#{template_type}" do - present klass.all, with: Entities::TemplatesList + templates = ::Kaminari.paginate_array(klass.all) + present paginate(templates), with: Entities::TemplatesList end desc 'Get the text for a specific template present in local filesystem' do diff --git a/lib/api/users.rb b/lib/api/users.rb index 82ac3886ac3b9feb3e6b8034f4cc41d3fa432219..fbc179536912b77a6f66a2c6b2b75f270d61050b 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -209,6 +209,7 @@ module API end params do requires :id, type: Integer, desc: 'The ID of the user' + use :pagination end get ':id/keys' do authenticated_as_admin! @@ -216,7 +217,7 @@ module API user = User.find_by(id: params[:id]) not_found!('User') unless user - present user.keys, with: Entities::SSHKey + present paginate(user.keys), with: Entities::SSHKey end desc 'Delete an existing SSH key from a specified user. Available only for admins.' do @@ -266,13 +267,14 @@ module API end params do requires :id, type: Integer, desc: 'The ID of the user' + use :pagination end get ':id/emails' do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user - present user.emails, with: Entities::Email + present paginate(user.emails), with: Entities::Email end desc 'Delete an email address of a specified user. Available only for admins.' do @@ -312,7 +314,7 @@ module API params do requires :id, type: Integer, desc: 'The ID of the user' end - put ':id/block' do + post ':id/block' do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user @@ -328,7 +330,7 @@ module API params do requires :id, type: Integer, desc: 'The ID of the user' end - put ':id/unblock' do + post ':id/unblock' do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user @@ -373,8 +375,11 @@ module API desc "Get the currently authenticated user's SSH keys" do success Entities::SSHKey end + params do + use :pagination + end get "keys" do - present current_user.keys, with: Entities::SSHKey + present paginate(current_user.keys), with: Entities::SSHKey end desc 'Get a single key owned by currently authenticated user' do @@ -423,8 +428,11 @@ module API desc "Get the currently authenticated user's email addresses" do success Entities::Email end + params do + use :pagination + end get "emails" do - present current_user.emails, with: Entities::Email + present paginate(current_user.emails), with: Entities::Email end desc 'Get a single email address owned by the currently authenticated user' do diff --git a/lib/api/v3/boards.rb b/lib/api/v3/boards.rb new file mode 100644 index 0000000000000000000000000000000000000000..31d708bc2c83a6b3c5cad7024de7ed1ff051f61a --- /dev/null +++ b/lib/api/v3/boards.rb @@ -0,0 +1,51 @@ +module API + module V3 + class Boards < Grape::API + before { authenticate! } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + desc 'Get all project boards' do + detail 'This feature was introduced in 8.13' + success ::API::Entities::Board + end + get ':id/boards' do + authorize!(:read_board, user_project) + present user_project.boards, with: ::API::Entities::Board + end + + params do + requires :board_id, type: Integer, desc: 'The ID of a board' + end + segment ':id/boards/:board_id' do + helpers do + def project_board + board = user_project.boards.first + + if params[:board_id] == board.id + board + else + not_found!('Board') + end + end + + def board_lists + project_board.lists.destroyable + end + end + + desc 'Get the lists of a project board' do + detail 'Does not include `done` list. This feature was introduced in 8.13' + success ::API::Entities::List + end + get '/lists' do + authorize!(:read_board, user_project) + present board_lists, with: ::API::Entities::List + end + end + end + end + end +end diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb new file mode 100644 index 0000000000000000000000000000000000000000..733c6b21be5d50124b6831c0b331761b7d2e9a97 --- /dev/null +++ b/lib/api/v3/branches.rb @@ -0,0 +1,24 @@ +require 'mime/types' + +module API + module V3 + class Branches < Grape::API + before { authenticate! } + before { authorize! :download_code, user_project } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + desc 'Get a project repository branches' do + success ::API::Entities::RepoBranch + end + get ":id/repository/branches" do + branches = user_project.repository.branches.sort_by(&:name) + + present branches, with: ::API::Entities::RepoBranch, project: user_project + end + end + end + end +end diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb index 081d45165e842ba9b88326305872bee91ff32cd5..ba5b6fdbe52170782d05578c05b968cd89002d07 100644 --- a/lib/api/v3/issues.rb +++ b/lib/api/v3/issues.rb @@ -16,7 +16,8 @@ module API labels = args.delete(:labels) args[:label_name] = labels if match_all_labels - args[:search] = "#{Issue.reference_prefix}#{args.delete(:iid)}" if args.key?(:iid) + # IssuesFinder expects iids + args[:iids] = args.delete(:iid) if args.key?(:iid) issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb new file mode 100644 index 0000000000000000000000000000000000000000..5c3261311bf7bcfd7dd6a7bf63118440f28bd98b --- /dev/null +++ b/lib/api/v3/labels.rb @@ -0,0 +1,19 @@ +module API + module V3 + class Labels < Grape::API + before { authenticate! } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + desc 'Get all labels of the project' do + success ::API::Entities::Label + end + get ':id/labels' do + present available_labels, with: ::API::Entities::Label, current_user: current_user, project: user_project + end + end + end + end +end diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb new file mode 100644 index 0000000000000000000000000000000000000000..3549ea225eff085e08c0babfe78f21e776cd0561 --- /dev/null +++ b/lib/api/v3/repositories.rb @@ -0,0 +1,55 @@ +require 'mime/types' + +module API + module V3 + class Repositories < Grape::API + before { authorize! :download_code, user_project } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + helpers do + def handle_project_member_errors(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + end + not_found! + end + end + + desc 'Get a project repository tree' do + success ::API::Entities::RepoTreeObject + end + params do + optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' + optional :path, type: String, desc: 'The path of the tree' + optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree' + end + get ':id/repository/tree' do + ref = params[:ref_name] || user_project.try(:default_branch) || 'master' + path = params[:path] || nil + + commit = user_project.commit(ref) + not_found!('Tree') unless commit + + tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive]) + + present tree.sorted_entries, with: ::API::Entities::RepoTreeObject + end + + desc 'Get repository contributors' do + success ::API::Entities::Contributor + end + get ':id/repository/contributors' do + begin + present user_project.repository.contributors, + with: ::API::Entities::Contributor + rescue + not_found! + end + end + end + end + end +end diff --git a/lib/api/v3/system_hooks.rb b/lib/api/v3/system_hooks.rb new file mode 100644 index 0000000000000000000000000000000000000000..391510b9ee0bab9e69959b3647d03b10d9351cc7 --- /dev/null +++ b/lib/api/v3/system_hooks.rb @@ -0,0 +1,19 @@ +module API + module V3 + class SystemHooks < Grape::API + before do + authenticate! + authenticated_as_admin! + end + + resource :hooks do + desc 'Get the list of system hooks' do + success ::API::Entities::Hook + end + get do + present SystemHook.all, with: ::API::Entities::Hook + end + end + end + end +end diff --git a/lib/api/v3/tags.rb b/lib/api/v3/tags.rb new file mode 100644 index 0000000000000000000000000000000000000000..016e3d8693243003861c8aca8309c13be1ceafdc --- /dev/null +++ b/lib/api/v3/tags.rb @@ -0,0 +1,20 @@ +module API + module V3 + class Tags < Grape::API + before { authorize! :download_code, user_project } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + desc 'Get a project repository tags' do + success ::API::Entities::RepoTag + end + get ":id/repository/tags" do + tags = user_project.repository.tags.sort_by(&:name).reverse + present tags, with: ::API::Entities::RepoTag, project: user_project + end + end + end + end +end diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb new file mode 100644 index 0000000000000000000000000000000000000000..e05e457a5dfa9554d96d3355dc44c6eff13e1bcb --- /dev/null +++ b/lib/api/v3/users.rb @@ -0,0 +1,96 @@ +module API + module V3 + class Users < Grape::API + include PaginationParams + + before do + allow_access_with_scope :read_user if request.get? + authenticate! + end + + resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do + desc 'Get the SSH keys of a specified user. Available only for admins.' do + success ::API::Entities::SSHKey + end + params do + requires :id, type: Integer, desc: 'The ID of the user' + use :pagination + end + get ':id/keys' do + authenticated_as_admin! + + user = User.find_by(id: params[:id]) + not_found!('User') unless user + + present paginate(user.keys), with: ::API::Entities::SSHKey + end + + desc 'Get the emails addresses of a specified user. Available only for admins.' do + success ::API::Entities::Email + end + params do + requires :id, type: Integer, desc: 'The ID of the user' + use :pagination + end + get ':id/emails' do + authenticated_as_admin! + user = User.find_by(id: params[:id]) + not_found!('User') unless user + + present user.emails, with: ::API::Entities::Email + end + + desc 'Block a user. Available only for admins.' + params do + requires :id, type: Integer, desc: 'The ID of the user' + end + put ':id/block' do + authenticated_as_admin! + user = User.find_by(id: params[:id]) + not_found!('User') unless user + + if !user.ldap_blocked? + user.block + else + forbidden!('LDAP blocked users cannot be modified by the API') + end + end + + desc 'Unblock a user. Available only for admins.' + params do + requires :id, type: Integer, desc: 'The ID of the user' + end + put ':id/unblock' do + authenticated_as_admin! + user = User.find_by(id: params[:id]) + not_found!('User') unless user + + if user.ldap_blocked? + forbidden!('LDAP blocked users cannot be unblocked by the API') + else + user.activate + end + end + end + + resource :user do + desc "Get the currently authenticated user's SSH keys" do + success ::API::Entities::SSHKey + end + params do + use :pagination + end + get "keys" do + present current_user.keys, with: ::API::Entities::SSHKey + end + + desc "Get the currently authenticated user's email addresses" do + success ::API::Entities::Email + end + get "emails" do + present current_user.emails, with: ::API::Entities::Email + end + end + end + end +end diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 955d857c67996e9a08b48bcf26cc7a0dd3ecd211..3b15ff6566fc72293977d2f22b788ed0d02f47ad 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -33,7 +33,12 @@ module Banzai # Returns a String replaced with the return of the block. def self.references_in(text, pattern = object_class.reference_pattern) text.gsub(pattern) do |match| - yield match, $~[object_sym].to_i, $~[:project], $~[:namespace], $~ + symbol = $~[object_sym] + if object_class.reference_valid?(symbol) + yield match, symbol.to_i, $~[:project], $~[:namespace], $~ + else + match + end end end diff --git a/lib/gitlab/chat_commands/presenters/issue_new.rb b/lib/gitlab/chat_commands/presenters/issue_new.rb index 0d31660039a597ca18e37f9b972fa72a3a4dc69c..3674ba25641e4dcda131059bf2ef14eb3679ae19 100644 --- a/lib/gitlab/chat_commands/presenters/issue_new.rb +++ b/lib/gitlab/chat_commands/presenters/issue_new.rb @@ -10,7 +10,7 @@ module Gitlab private - def new_issue + def new_issue { attachments: [ { @@ -38,7 +38,7 @@ module Gitlab end def project_link - "[#{project.name_with_namespace}](#{projects_url(project)})" + "[#{project.name_with_namespace}](#{project.web_url})" end def author_profile_link diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb index d1bc2055ba8cf78b5a81e1792b19d9315f17dcbb..1e52b6614a1520412f0215b0e3d2534f6b7bab2c 100644 --- a/lib/gitlab/cycle_analytics/code_stage.rb +++ b/lib/gitlab/cycle_analytics/code_stage.rb @@ -13,6 +13,10 @@ module Gitlab :code end + def legend + "Related Merge Requests" + end + def description "Time until first merge request" end diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb index d2068fbc38fe5718385592c00236939fcc163b6d..213994988a5a5f24c42af5e720274f9de9affb2d 100644 --- a/lib/gitlab/cycle_analytics/issue_stage.rb +++ b/lib/gitlab/cycle_analytics/issue_stage.rb @@ -14,6 +14,10 @@ module Gitlab :issue end + def legend + "Related Issues" + end + def description "Time before an issue gets scheduled" end diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb index 3b4dfc6a30e1a1e86f188477d1e69dd6435815b9..45d51d30ccc6bc8d14c18ca1aa68c9af83a495a7 100644 --- a/lib/gitlab/cycle_analytics/plan_stage.rb +++ b/lib/gitlab/cycle_analytics/plan_stage.rb @@ -14,6 +14,10 @@ module Gitlab :plan end + def legend + "Related Commits" + end + def description "Time before an issue starts implementation" end diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb index 2a6bcc80116ab24b12f89f7d76eb2ed4e9771026..9f387a0294533ba19b0520dabbbad0b1560c3a8f 100644 --- a/lib/gitlab/cycle_analytics/production_stage.rb +++ b/lib/gitlab/cycle_analytics/production_stage.rb @@ -15,6 +15,10 @@ module Gitlab :production end + def legend + "Related Issues" + end + def description "From issue creation until deploy to production" end diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb index fbaa3010d8138c96fc25179d367e0bfcca798bf1..4744be834de27285c6ce00723100dd67db43f5d9 100644 --- a/lib/gitlab/cycle_analytics/review_stage.rb +++ b/lib/gitlab/cycle_analytics/review_stage.rb @@ -13,6 +13,10 @@ module Gitlab :review end + def legend + "Relative Merged Requests" + end + def description "Time between merge request creation and merge/close" end diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb index 945909a4d626b16ebef23c23fa5f45c1ec874038..3cdbe04fbaf1db1333caa560b79ba1b954823d19 100644 --- a/lib/gitlab/cycle_analytics/staging_stage.rb +++ b/lib/gitlab/cycle_analytics/staging_stage.rb @@ -14,6 +14,10 @@ module Gitlab :staging end + def legend + "Relative Deployed Builds" + end + def description "From merge request merge until deploy to production" end diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb index 0079d56e0e4770ceb59262a78ea5ba6fb6d50627..e96943833bc46401fe410870640a855fc5e98d32 100644 --- a/lib/gitlab/cycle_analytics/test_stage.rb +++ b/lib/gitlab/cycle_analytics/test_stage.rb @@ -13,6 +13,10 @@ module Gitlab :test end + def legend + "Relative Builds Trigger by Commits" + end + def description "Total test time for all commits/merges" end diff --git a/lib/gitlab/data_builder/build.rb b/lib/gitlab/data_builder/build.rb index 6548e6475c60fcd5da5aca3a92fc225f10b5d2e1..f78106f5b1023c1c3de274e8ebefaec058835130 100644 --- a/lib/gitlab/data_builder/build.rb +++ b/lib/gitlab/data_builder/build.rb @@ -8,6 +8,8 @@ module Gitlab commit = build.pipeline user = build.user + author_url = build_author_url(build.commit, commit) + data = { object_kind: 'build', @@ -43,6 +45,7 @@ module Gitlab message: commit.git_commit_message, author_name: commit.git_author_name, author_email: commit.git_author_email, + author_url: author_url, status: commit.status, duration: commit.duration, started_at: commit.started_at, @@ -62,6 +65,13 @@ module Gitlab data end + + private + + def build_author_url(commit, pipeline) + author = commit.try(:author) + author ? Gitlab::Routing.url_helpers.user_url(author) : "mailto:#{pipeline.git_author_email}" + end end end end diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index f00c7460e82abba81710514b1da68c866c60dd71..90942774a2ec009b9c92d23cda1d4566c1a601f6 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -15,14 +15,6 @@ module Gitlab execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all)) end - def git_unbundle(repo_path:, bundle_path:) - execute(%W(#{git_bin_path} clone --bare #{bundle_path} #{repo_path})) - end - - def git_restore_hooks - execute(%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args) - end - def mkdir_p(path) FileUtils.mkdir_p(path, mode: DEFAULT_MODE) FileUtils.chmod(DEFAULT_MODE, path) @@ -56,10 +48,6 @@ module Gitlab FileUtils.copy_entry(source, destination) true end - - def repository_storage_paths_args - Gitlab.config.repositories.storages.values - end end end end diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index 48a9a6fa5e2c8b88b726c75861a4c8fba08cb409..c824d3ea9fc52b59bf273b0e2ea73fc5f02db048 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -2,6 +2,7 @@ module Gitlab module ImportExport class RepoRestorer include Gitlab::ImportExport::CommandLineUtil + include Gitlab::ShellAdapter def initialize(project:, shared:, path_to_bundle:) @project = project @@ -12,29 +13,11 @@ module Gitlab def restore return true unless File.exist?(@path_to_bundle) - mkdir_p(path_to_repo) - - git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) && repo_restore_hooks + gitlab_shell.import_repository(@project.repository_storage_path, @project.path_with_namespace, @path_to_bundle) rescue => e @shared.error(e) false end - - private - - def path_to_repo - @project.repository.path_to_repo - end - - def repo_restore_hooks - return true if wiki? - - git_restore_hooks - end - - def wiki? - @project.class.name == 'ProjectWiki' - end end end end diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 942cedd6cd493895ca6d8d0c18ceb3cb2390b2ab..3faa336f142112d91b5f91999044e855d622148a 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -80,8 +80,10 @@ module Gitlab # import_repository("/path/to/storage", "gitlab/gitlab-ci", "https://github.com/randx/six.git") # def import_repository(storage, name, url) + # Timeout should be less than 900 ideally, to prevent the memory killer + # to silently kill the process without knowing we are timing out here. output, status = Popen::popen([gitlab_shell_projects_path, 'import-project', - storage, "#{name}.git", url, '900']) + storage, "#{name}.git", url, '800']) raise Error, output unless status.zero? true end diff --git a/lib/gitlab/slash_commands/extractor.rb b/lib/gitlab/slash_commands/extractor.rb index a672e5e485528a58a1d1f562f95f80b4b4fa3ea3..6dbb467d70de6f819c69489797bd9774909bebc5 100644 --- a/lib/gitlab/slash_commands/extractor.rb +++ b/lib/gitlab/slash_commands/extractor.rb @@ -103,7 +103,7 @@ module Gitlab (?<cmd>#{Regexp.union(names)}) (?: [ ] - (?<arg>[^\/\n]*) + (?<arg>[^\n]*) )? (?:\n|$) ) diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb index 9e01f02029c1a7fd0e7e9ef0fcb1fa13fb0c90e5..b85f70e450ec58bf3682e355d40bee11244e648d 100644 --- a/lib/gitlab/snippet_search_results.rb +++ b/lib/gitlab/snippet_search_results.rb @@ -31,11 +31,11 @@ module Gitlab private def snippet_titles - limit_snippets.search(query).order('updated_at DESC') + limit_snippets.search(query).order('updated_at DESC').includes(:author) end def snippet_blobs - limit_snippets.search_code(query).order('updated_at DESC') + limit_snippets.search_code(query).order('updated_at DESC').includes(:author) end def default_scope diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb deleted file mode 100644 index 19ab76ae80f6a6b26d3e5c694e76511b14700499..0000000000000000000000000000000000000000 --- a/lib/gitlab/themes.rb +++ /dev/null @@ -1,87 +0,0 @@ -module Gitlab - # Module containing GitLab's application theme definitions and helper methods - # for accessing them. - module Themes - extend self - - # Theme ID used when no `default_theme` configuration setting is provided. - APPLICATION_DEFAULT = 2 - - # Struct class representing a single Theme - Theme = Struct.new(:id, :name, :css_class) - - # All available Themes - THEMES = [ - Theme.new(1, 'Graphite', 'ui_graphite'), - Theme.new(2, 'Charcoal', 'ui_charcoal'), - Theme.new(3, 'Green', 'ui_green'), - Theme.new(4, 'Black', 'ui_black'), - Theme.new(5, 'Violet', 'ui_violet'), - Theme.new(6, 'Blue', 'ui_blue') - ].freeze - - # Convenience method to get a space-separated String of all the theme - # classes that might be applied to the `body` element - # - # Returns a String - def body_classes - THEMES.collect(&:css_class).uniq.join(' ') - end - - # Get a Theme by its ID - # - # If the ID is invalid, returns the default Theme. - # - # id - Integer ID - # - # Returns a Theme - def by_id(id) - THEMES.detect { |t| t.id == id } || default - end - - # Returns the number of defined Themes - def count - THEMES.size - end - - # Get the default Theme - # - # Returns a Theme - def default - by_id(default_id) - end - - # Iterate through each Theme - # - # Yields the Theme object - def each(&block) - THEMES.each(&block) - end - - # Get the Theme for the specified user, or the default - # - # user - User record - # - # Returns a Theme - def for_user(user) - if user - by_id(user.theme_id) - else - default - end - end - - private - - def default_id - id = Gitlab.config.gitlab.default_theme.to_i - - # Prevent an invalid configuration setting from causing an infinite loop - if id < THEMES.first.id || id > THEMES.last.id - APPLICATION_DEFAULT - else - id - end - end - end -end diff --git a/lib/gitlab/upgrader.rb b/lib/gitlab/upgrader.rb index e78d0c34a02e5972ceafc7c43dd3e77311d4d01c..4cc34e344606be2385d5a4cad9476329c3b433c7 100644 --- a/lib/gitlab/upgrader.rb +++ b/lib/gitlab/upgrader.rb @@ -61,13 +61,16 @@ module Gitlab "Switch to new version" => %W(#{Gitlab.config.git.bin_path} checkout v#{latest_version}), "Install gems" => %W(bundle), "Migrate DB" => %W(bundle exec rake db:migrate), - "Recompile assets" => %W(bundle exec rake gitlab:assets:clean gitlab:assets:compile), + "Recompile assets" => %W(bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile), "Clear cache" => %W(bundle exec rake cache:clear) } end def env - { 'RAILS_ENV' => 'production' } + { + 'RAILS_ENV' => 'production', + 'NODE_ENV' => 'production' + } end def upgrade diff --git a/lib/tasks/eslint.rake b/lib/tasks/eslint.rake index 2514b050695dcd4d2cd017fd1c85bb840e082358..51f5d76810257a8eb39b15b7c86acf246b172c29 100644 --- a/lib/tasks/eslint.rake +++ b/lib/tasks/eslint.rake @@ -1,7 +1,8 @@ unless Rails.env.production? desc "GitLab | Run ESLint" - task :eslint do - system("yarn", "run", "eslint") + task eslint: ['yarn:check'] do + unless system('yarn run eslint') + abort('rake eslint failed') + end end end - diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake index b6ef826019177a4586fb356ab4a6027ca1d1afe0..3eb5fc07b3c20a562e774b3dd1ad53017f0da6a2 100644 --- a/lib/tasks/gitlab/assets.rake +++ b/lib/tasks/gitlab/assets.rake @@ -1,21 +1,21 @@ namespace :gitlab do namespace :assets do desc 'GitLab | Assets | Compile all frontend assets' - task :compile do - Rake::Task['assets:precompile'].invoke - Rake::Task['webpack:compile'].invoke - Rake::Task['gitlab:assets:fix_urls'].invoke - end + task compile: [ + 'yarn:check', + 'assets:precompile', + 'webpack:compile', + 'gitlab:assets:fix_urls' + ] desc 'GitLab | Assets | Clean up old compiled frontend assets' - task :clean do - Rake::Task['assets:clean'].invoke - end + task clean: ['assets:clean'] desc 'GitLab | Assets | Remove all compiled frontend assets' - task :purge do - Rake::Task['assets:clobber'].invoke - end + task purge: ['assets:clobber'] + + desc 'GitLab | Assets | Uninstall frontend dependencies' + task purge_modules: ['yarn:clobber'] desc 'GitLab | Assets | Fix all absolute url references in CSS' task :fix_urls do diff --git a/lib/tasks/karma.rake b/lib/tasks/karma.rake index 35cfed9dc758e4598d82bee2c6ec867a578a20b4..40465ea3bf0379f2d7edc95e575a95e92f152fe4 100644 --- a/lib/tasks/karma.rake +++ b/lib/tasks/karma.rake @@ -1,6 +1,4 @@ unless Rails.env.production? - Rake::Task['karma'].clear if Rake::Task.task_defined?('karma') - namespace :karma do desc 'GitLab | Karma | Generate fixtures for JavaScript tests' RSpec::Core::RakeTask.new(:fixtures) do |t| @@ -10,7 +8,7 @@ unless Rails.env.production? end desc 'GitLab | Karma | Run JavaScript tests' - task :tests do + task tests: ['yarn:check'] do sh "yarn run karma" do |ok, res| abort('rake karma:tests failed') unless ok end @@ -18,8 +16,5 @@ unless Rails.env.production? end desc 'GitLab | Karma | Shortcut for karma:fixtures and karma:tests' - task :karma do - Rake::Task['karma:fixtures'].invoke - Rake::Task['karma:tests'].invoke - end + task karma: ['karma:fixtures', 'karma:tests'] end diff --git a/lib/tasks/yarn.rake b/lib/tasks/yarn.rake new file mode 100644 index 0000000000000000000000000000000000000000..2ac88a039e70013916a5967e38a8b2c3fb300992 --- /dev/null +++ b/lib/tasks/yarn.rake @@ -0,0 +1,40 @@ + +namespace :yarn do + desc 'Ensure Yarn is installed' + task :available do + unless system('yarn --version', out: File::NULL) + warn( + 'Error: Yarn executable was not detected in the system.'.color(:red), + 'Download Yarn at https://yarnpkg.com/en/docs/install'.color(:green) + ) + abort + end + end + + desc 'Ensure Node dependencies are installed' + task check: ['yarn:available'] do + unless system('yarn check --ignore-engines', out: File::NULL) + warn( + 'Error: You have unmet dependencies. (`yarn check` command failed)'.color(:red), + 'Run `yarn install` to install missing modules.'.color(:green) + ) + abort + end + end + + desc 'Install Node dependencies with Yarn' + task install: ['yarn:available'] do + unless system('yarn install --pure-lockfile --ignore-engines') + abort 'Error: Unable to install node modules.'.color(:red) + end + end + + desc 'Remove Node dependencies' + task :clobber do + warn 'Purging ./node_modules directory'.color(:red) + FileUtils.rm_rf 'node_modules' + end +end + +desc 'Install Node dependencies with Yarn' +task yarn: ['yarn:install'] diff --git a/package.json b/package.json index 08bde1bc3139d4f138968509df42b2062d17ade9..ad0aaef1897115e86d4e1ed555e93dea6e804e47 100644 --- a/package.json +++ b/package.json @@ -15,27 +15,24 @@ "babel-loader": "^6.2.10", "babel-preset-es2015": "^6.22.0", "babel-preset-stage-2": "^6.22.0", - "bootstrap-sass": "3.3.6", + "bootstrap-sass": "^3.3.6", "compression-webpack-plugin": "^0.3.2", - "d3": "3.5.11", - "dropzone": "4.2.0", + "d3": "^3.5.11", + "dropzone": "^4.2.0", "es6-promise": "^4.0.5", - "imports-loader": "^0.6.5", - "jquery": "2.2.1", - "jquery-ui": "github:jquery/jquery-ui#1.11.4", - "jquery-ujs": "1.2.1", + "jquery": "^2.2.1", + "jquery-ui": "git+https://github.com/jquery/jquery-ui#1.11.4", + "jquery-ujs": "^1.2.1", "js-cookie": "^2.1.3", - "karma-mocha-reporter": "^2.2.2", - "mousetrap": "1.4.6", + "mousetrap": "^1.4.6", "pikaday": "^1.5.1", "select2": "3.5.2-browserify", "stats-webpack-plugin": "^0.4.3", "timeago.js": "^2.0.5", - "underscore": "1.8.3", - "vue": "2.0.3", - "vue-resource": "0.9.3", - "webpack": "^2.2.1", - "webpack-dev-server": "^2.3.0" + "underscore": "^1.8.3", + "vue": "^2.0.3", + "vue-resource": "^0.9.3", + "webpack": "^2.2.1" }, "devDependencies": { "babel-plugin-istanbul": "^4.0.0", @@ -51,9 +48,11 @@ "karma": "^1.4.1", "karma-coverage-istanbul-reporter": "^0.2.0", "karma-jasmine": "^1.1.0", + "karma-mocha-reporter": "^2.2.2", "karma-phantomjs-launcher": "^1.0.2", "karma-sourcemap-loader": "^0.3.7", - "karma-webpack": "^2.0.2" + "karma-webpack": "^2.0.2", + "webpack-dev-server": "^2.3.0" }, "nyc": { "exclude": [ diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b5fe40d05108ca7b477c4857a4e9ccf76e148551 --- /dev/null +++ b/spec/controllers/admin/runners_controller_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +describe Admin::RunnersController do + let(:runner) { create(:ci_runner) } + + before do + sign_in(create(:admin)) + end + + describe '#index' do + it 'lists all runners' do + get :index + + expect(response).to have_http_status(200) + end + end + + describe '#show' do + it 'shows a particular runner' do + get :show, id: runner.id + + expect(response).to have_http_status(200) + end + + it 'shows 404 for unknown runner' do + get :show, id: 0 + + expect(response).to have_http_status(404) + end + end + + describe '#update' do + it 'updates the runner and ticks the queue' do + new_desc = runner.description.swapcase + + expect do + post :update, id: runner.id, runner: { description: new_desc } + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.description).to eq(new_desc) + end + end + + describe '#destroy' do + it 'destroys the runner' do + delete :destroy, id: runner.id + + expect(response).to have_http_status(302) + expect(Ci::Runner.find_by(id: runner.id)).to be_nil + end + end + + describe '#resume' do + it 'marks the runner as active and ticks the queue' do + runner.update(active: false) + + expect do + post :resume, id: runner.id + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.active).to eq(true) + end + end + + describe '#pause' do + it 'marks the runner as inactive and ticks the queue' do + runner.update(active: true) + + expect do + post :pause, id: runner.id + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.active).to eq(false) + end + end +end diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb index 79ef3a1adadab1c60ed30520d93af697e1a1c6b3..0a3ac9f95126ebe3e1a4bc3bb6c6f22c814659b1 100644 --- a/spec/controllers/dashboard/todos_controller_spec.rb +++ b/spec/controllers/dashboard/todos_controller_spec.rb @@ -1,16 +1,19 @@ require 'spec_helper' describe Dashboard::TodosController do + include ApiHelpers + let(:user) { create(:user) } + let(:author) { create(:user) } let(:project) { create(:empty_project) } let(:todo_service) { TodoService.new } - describe 'GET #index' do - before do - sign_in(user) - project.team << [user, :developer] - end + before do + sign_in(user) + project.team << [user, :developer] + end + describe 'GET #index' do context 'when using pagination' do let(:last_page) { user.todos.page.total_pages } let!(:issues) { create_list(:issue, 2, project: project, assignee: user) } @@ -34,4 +37,16 @@ describe Dashboard::TodosController do end end end + + describe 'PATCH #restore' do + let(:todo) { create(:todo, :done, user: user, project: project, author: author) } + + it 'restores the todo to pending state' do + patch :restore, id: todo.id + + expect(todo.reload).to be_pending + expect(response).to have_http_status(200) + expect(json_response).to eq({ "count" => 1, "done_count" => 0 }) + end + end end diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb index 8f02003992a2a2a65e192c3f91ce0b4a4cd453e7..7b3aa0491c72b35712a902a67fc94fc7ca2ec60b 100644 --- a/spec/controllers/profiles/preferences_controller_spec.rb +++ b/spec/controllers/profiles/preferences_controller_spec.rb @@ -25,8 +25,7 @@ describe Profiles::PreferencesController do def go(params: {}, format: :js) params.reverse_merge!( color_scheme_id: '1', - dashboard: 'stars', - theme_id: '1' + dashboard: 'stars' ) patch :update, user: params, format: format @@ -41,8 +40,7 @@ describe Profiles::PreferencesController do it "changes the user's preferences" do prefs = { color_scheme_id: '1', - dashboard: 'stars', - theme_id: '2' + dashboard: 'stars' }.with_indifferent_access expect(user).to receive(:update_attributes).with(prefs) diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 7ac1d62d1b1a63ddee9dae16289ddba4d062c752..84d119f1867d2ab76861f5208a146500f81af067 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -3,9 +3,12 @@ require 'spec_helper' describe Projects::EnvironmentsController do include ApiHelpers - let(:environment) { create(:environment) } - let(:project) { environment.project } - let(:user) { create(:user) } + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + + let(:environment) do + create(:environment, name: 'production', project: project) + end before do project.team << [user, :master] @@ -22,14 +25,58 @@ describe Projects::EnvironmentsController do end end - context 'when requesting JSON response' do - it 'responds with correct JSON' do - get :index, environment_params(format: :json) + context 'when requesting JSON response for folders' do + before do + create(:environment, project: project, + name: 'staging/review-1', + state: :available) + + create(:environment, project: project, + name: 'staging/review-2', + state: :available) + + create(:environment, project: project, + name: 'staging/review-3', + state: :stopped) + end + + let(:environments) { json_response['environments'] } + + context 'when requesting available environments scope' do + before do + get :index, environment_params(format: :json, scope: :available) + end + + it 'responds with a payload describing available environments' do + expect(environments.count).to eq 2 + expect(environments.first['name']).to eq 'production' + expect(environments.second['name']).to eq 'staging' + expect(environments.second['size']).to eq 2 + expect(environments.second['latest']['name']).to eq 'staging/review-2' + end - first_environment = json_response.first + it 'contains values describing environment scopes sizes' do + expect(json_response['available_count']).to eq 3 + expect(json_response['stopped_count']).to eq 1 + end + end - expect(first_environment).not_to be_empty - expect(first_environment['name']). to eq environment.name + context 'when requesting stopped environments scope' do + before do + get :index, environment_params(format: :json, scope: :stopped) + end + + it 'responds with a payload describing stopped environments' do + expect(environments.count).to eq 1 + expect(environments.first['name']).to eq 'staging' + expect(environments.first['size']).to eq 1 + expect(environments.first['latest']['name']).to eq 'staging/review-3' + end + + it 'contains values describing environment scopes sizes' do + expect(json_response['available_count']).to eq 3 + expect(json_response['stopped_count']).to eq 1 + end end end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index bfd134e406e5d82d0e2a816b7c479ac43e472032..f84f922ba5e046571a3e92d32d7f97c895972874 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -1143,15 +1143,15 @@ describe Projects::MergeRequestsController do end end - context 'when no special status for MR' do + context 'when MR does not have special state' do let(:merge_request) { create(:merge_request, source_project: project) } it 'returns an OK response' do expect(response).to have_http_status(:ok) end - it 'sets status to nil' do - expect(assigns(:status)).to be_nil + it 'sets status to success' do + expect(assigns(:status)).to eq(:success) expect(response).to render_template('merge') end end diff --git a/spec/controllers/projects/runners_controller_spec.rb b/spec/controllers/projects/runners_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..0fa249e440585e9931c211f5df0a1a787c4bda3c --- /dev/null +++ b/spec/controllers/projects/runners_controller_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe Projects::RunnersController do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:runner) { create(:ci_runner) } + + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + id: runner + } + end + + before do + sign_in(user) + project.add_master(user) + project.runners << runner + end + + describe '#update' do + it 'updates the runner and ticks the queue' do + new_desc = runner.description.swapcase + + expect do + post :update, params.merge(runner: { description: new_desc } ) + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.description).to eq(new_desc) + end + end + + describe '#destroy' do + it 'destroys the runner' do + delete :destroy, params + + expect(response).to have_http_status(302) + expect(Ci::Runner.find_by(id: runner.id)).to be_nil + end + end + + describe '#resume' do + it 'marks the runner as active and ticks the queue' do + runner.update(active: false) + + expect do + post :resume, params + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.active).to eq(true) + end + end + + describe '#pause' do + it 'marks the runner as inactive and ticks the queue' do + runner.update(active: true) + + expect do + post :pause, params + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.active).to eq(false) + end + end +end diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index 19e948d8fb8294a38e16b6c1c8f96094bbe32e20..77ee10a1e151ee953f0ebeecf42a885af07c18c9 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -206,4 +206,37 @@ describe Projects::SnippetsController do end end end + + describe 'GET #raw' do + let(:project_snippet) do + create( + :project_snippet, :public, + project: project, + author: user, + content: "first line\r\nsecond line\r\nthird line" + ) + end + + context 'CRLF line ending' do + let(:params) do + { + namespace_id: project.namespace.path, + project_id: project.path, + id: project_snippet.to_param + } + end + + it 'returns LF line endings by default' do + get :raw, params + + expect(response.body).to eq("first line\nsecond line\nthird line") + end + + it 'does not convert line endings when parameter present' do + get :raw, params.merge(line_ending: :raw) + + expect(response.body).to eq("first line\r\nsecond line\r\nthird line") + end + end + end end diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index dadcb90cfc2350b362319a443a1fb97a2998d7f5..f90c0d76ceba6626f5186848c3a8d0368da95826 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -286,6 +286,24 @@ describe SnippetsController do expect(assigns(:snippet)).to eq(personal_snippet) expect(response).to have_http_status(200) end + + context 'CRLF line ending' do + let(:personal_snippet) do + create(:personal_snippet, :public, author: user, content: "first line\r\nsecond line\r\nthird line") + end + + it 'returns LF line endings by default' do + get action, id: personal_snippet.to_param + + expect(response.body).to eq("first line\nsecond line\nthird line") + end + + it 'does not convert line endings when parameter present' do + get action, id: personal_snippet.to_param, line_ending: :raw + + expect(response.body).to eq("first line\r\nsecond line\r\nthird line") + end + end end context 'when not signed in' do diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 0397d5d400185349606de1693b2687d5f2ecae3f..a90534d10ba2bfb6e294d4cfe24f42d1e1cf9a36 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -89,8 +89,9 @@ FactoryGirl.define do tag true end - factory :ci_build_with_coverage do + trait :coverage do coverage 99.9 + coverage_regex '/(d+)/' end trait :trace do @@ -99,6 +100,16 @@ FactoryGirl.define do end end + trait :erased do + erased_at Time.now + erased_by factory: :user + end + + trait :queued do + queued_at Time.now + runner factory: :ci_runner + end + trait :artifacts do after(:create) do |build, _| build.artifacts_file = @@ -128,5 +139,17 @@ FactoryGirl.define do build.save! end end + + trait :with_commit do + after(:build) do |build| + allow(build).to receive(:commit).and_return build(:commit, :without_author) + end + end + + trait :with_commit_and_author do + after(:build) do |build| + allow(build).to receive(:commit).and_return build(:commit) + end + end end end diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb index ac6eb0a7897440e504d5ca5367a60817b89c904b..89e260cf65bec04355ec6a783a37be3fe09f1438 100644 --- a/spec/factories/commits.rb +++ b/spec/factories/commits.rb @@ -8,5 +8,15 @@ FactoryGirl.define do initialize_with do new(git_commit, project) end + + after(:build) do |commit| + allow(commit).to receive(:author).and_return build(:author) + end + + trait :without_author do + after(:build) do |commit| + allow(commit).to receive(:author).and_return nil + end + end end end diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index b4e4cd97780d55d6017def67444434bc4f4a2999..a5265f1b1897787ef2c2efa0d12d000dfe489cab 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -40,6 +40,10 @@ FactoryGirl.define do action { Todo::UNMERGEABLE } end + trait :pending do + state :pending + end + trait :done do state :done end diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb index 7fcfe5a54c7681f8fee0e55195c59391227f14c7..340884fc986835dd6133470d1582bc178bb463b9 100644 --- a/spec/features/admin/admin_abuse_reports_spec.rb +++ b/spec/features/admin/admin_abuse_reports_spec.rb @@ -30,5 +30,24 @@ describe "Admin::AbuseReports", feature: true, js: true do end end end + + describe 'if a many users have been reported for abuse' do + let(:report_count) { AbuseReport.default_per_page + 3 } + + before do + report_count.times do + create(:abuse_report, user: create(:user)) + end + end + + describe 'in the abuse report view' do + it 'presents information about abuse report' do + visit admin_abuse_reports_path + + expect(page).to have_selector('.pagination') + expect(page).to have_selector('.pagination .page', count: (report_count.to_f / AbuseReport.default_per_page).ceil) + end + end + end end end diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index f05fbe3d0625d47eedc7873f54dd3cecda00e2c2..5dcc7d35d82bae38a0efd941825cf90da50b7e03 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -18,7 +18,7 @@ describe "Admin Runners" do it 'has all necessary texts' do expect(page).to have_text "To register a new Runner" - expect(page).to have_text "Runners with last contact less than a minute ago: 1" + expect(page).to have_text "Runners with last contact more than a minute ago: 1" end describe 'search' do diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 7225f38b7e5525d2f0cbd4c98f070f42bf4af49a..1b25b51cfb2e0b4ad409dbb8bf952017765d84fb 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -28,6 +28,12 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_content('Welcome to your Issue Board!') end + it 'disables add issues button by default' do + button = page.find('.issue-boards-search button', text: 'Add issues') + + expect(button[:disabled]).to eq true + end + it 'hides the blank state when clicking nevermind button' do page.within(find('.board-blank-state')) do click_button("Nevermind, I'll use my own") diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 7651364703e0c7f974f0159cabcb49af2989c522..59e87b3f69cd4d562f4c7d003462c308cc270bfe 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -15,8 +15,11 @@ describe 'Issue Boards', feature: true, js: true do let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch]) } let(:board) { create(:board, project: project) } let!(:list) { create(:list, board: board, label: development, position: 0) } + let(:card) { first('.board').first('.card') } before do + Timecop.freeze + project.team << [user, :master] login_as(user) @@ -25,32 +28,28 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource end + after do + Timecop.return + end + it 'shows sidebar when clicking issue' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) expect(page).to have_selector('.issue-boards-sidebar') end it 'closes sidebar when clicking issue' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) expect(page).to have_selector('.issue-boards-sidebar') - page.within(first('.board')) do - first('.card').click - end + click_card(card) expect(page).not_to have_selector('.issue-boards-sidebar') end it 'closes sidebar when clicking close button' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) expect(page).to have_selector('.issue-boards-sidebar') @@ -60,9 +59,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'shows issue details when sidebar is open' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.issue-boards-sidebar') do expect(page).to have_content(issue2.title) @@ -70,15 +67,15 @@ describe 'Issue Boards', feature: true, js: true do end end - it 'removes card from board when clicking remove button' do - page.within(first('.board')) do - first('.card').click - end + it 'removes card from board when clicking ' do + click_card(card) page.within('.issue-boards-sidebar') do click_button 'Remove from board' end + wait_for_vue_resource + page.within(first('.board')) do expect(page).to have_selector('.card', count: 1) end @@ -86,9 +83,7 @@ describe 'Issue Boards', feature: true, js: true do context 'assignee' do it 'updates the issues assignee' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.assignee') do click_link 'Edit' @@ -104,17 +99,12 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_content(user.name) end - page.within(first('.board')) do - page.within(first('.card')) do - expect(page).to have_selector('.avatar') - end - end + expect(card).to have_selector('.avatar') end it 'removes the assignee' do - page.within(first('.board')) do - find('.card:nth-child(2)').click - end + card_two = first('.board').find('.card:nth-child(2)') + click_card(card_two) page.within('.assignee') do click_link 'Edit' @@ -130,17 +120,11 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_content('No assignee') end - page.within(first('.board')) do - page.within(find('.card:nth-child(2)')) do - expect(page).not_to have_selector('.avatar') - end - end + expect(card_two).not_to have_selector('.avatar') end it 'assignees to current user' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within(find('.assignee')) do expect(page).to have_content('No assignee') @@ -152,17 +136,11 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_content(user.name) end - page.within(first('.board')) do - page.within(first('.card')) do - expect(page).to have_selector('.avatar') - end - end + expect(card).to have_selector('.avatar') end it 'resets assignee dropdown' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.assignee') do click_link 'Edit' @@ -192,9 +170,7 @@ describe 'Issue Boards', feature: true, js: true do context 'milestone' do it 'adds a milestone' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.milestone') do click_link 'Edit' @@ -212,9 +188,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'removes a milestone' do - page.within(first('.board')) do - find('.card:nth-child(2)').click - end + click_card(card) page.within('.milestone') do click_link 'Edit' @@ -234,9 +208,7 @@ describe 'Issue Boards', feature: true, js: true do context 'due date' do it 'updates due date' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.due_date') do click_link 'Edit' @@ -252,9 +224,7 @@ describe 'Issue Boards', feature: true, js: true do context 'labels' do it 'adds a single label' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.labels') do click_link 'Edit' @@ -273,18 +243,12 @@ describe 'Issue Boards', feature: true, js: true do end end - page.within(first('.board')) do - page.within(first('.card')) do - expect(page).to have_selector('.label', count: 2) - expect(page).to have_content(bug.title) - end - end + expect(card).to have_selector('.label', count: 2) + expect(card).to have_content(bug.title) end it 'adds a multiple labels' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.labels') do click_link 'Edit' @@ -305,19 +269,13 @@ describe 'Issue Boards', feature: true, js: true do end end - page.within(first('.board')) do - page.within(first('.card')) do - expect(page).to have_selector('.label', count: 3) - expect(page).to have_content(bug.title) - expect(page).to have_content(regression.title) - end - end + expect(card).to have_selector('.label', count: 3) + expect(card).to have_content(bug.title) + expect(card).to have_content(regression.title) end it 'removes a label' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.labels') do click_link 'Edit' @@ -336,20 +294,14 @@ describe 'Issue Boards', feature: true, js: true do end end - page.within(first('.board')) do - page.within(first('.card')) do - expect(page).not_to have_selector('.label') - expect(page).not_to have_content(stretch.title) - end - end + expect(card).not_to have_selector('.label') + expect(card).not_to have_content(stretch.title) end end context 'subscription' do it 'changes issue subscription' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.subscription') do click_button 'Subscribe' @@ -358,4 +310,19 @@ describe 'Issue Boards', feature: true, js: true do end end end + + def click_card(card) + page.within(card) do + first('.card-number').click + end + + wait_for_sidebar + end + + def wait_for_sidebar + # loop until the CSS transition is complete + Timeout.timeout(0.5) do + loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290 + end + end end diff --git a/spec/features/dashboard/active_tab_spec.rb b/spec/features/dashboard/active_tab_spec.rb index 7d59fcac517dbdffcf4804841f2b15e6ae338ec3..ae750be4d4ae26a972356ecec4ee7928cbf3567e 100644 --- a/spec/features/dashboard/active_tab_spec.rb +++ b/spec/features/dashboard/active_tab_spec.rb @@ -1,14 +1,15 @@ require 'spec_helper' -RSpec.describe 'Dashboard Active Tab', feature: true do +RSpec.describe 'Dashboard Active Tab', js: true, feature: true do before do login_as :user end shared_examples 'page has active tab' do |title| it "#{title} tab" do - expect(page).to have_selector('.nav-sidebar li.active', count: 1) - expect(find('.nav-sidebar li.active')).to have_content(title) + find('.global-dropdown-toggle').trigger('click') + expect(page).to have_selector('.global-dropdown-menu li.active', count: 1) + expect(find('.global-dropdown-menu li.active')).to have_content(title) end end diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb index 41dcfe439c27de5570b539e8c213c596fdb527ce..a1718912fc6c6560982e8c8b532185a2eb0cf047 100644 --- a/spec/features/dashboard/issuables_counter_spec.rb +++ b/spec/features/dashboard/issuables_counter_spec.rb @@ -35,8 +35,9 @@ describe 'Navigation bar counter', feature: true, js: true, caching: true do end def expect_counters(issuable_type, count) - dashboard_count = find('li.active span.badge') - nav_count = find(".dashboard-shortcuts-#{issuable_type} span.count") + dashboard_count = find('li.active') + find('.global-dropdown-toggle').click + nav_count = find(".dashboard-shortcuts-#{issuable_type}") expect(nav_count).to have_content(count) expect(dashboard_count).to have_content(count) diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2db1cf71209ba2e1ef7ba00915ed4e2f1d49914a --- /dev/null +++ b/spec/features/dashboard/issues_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +RSpec.describe 'Dashboard Issues', feature: true do + let(:current_user) { create :user } + let(:public_project) { create(:empty_project, :public) } + let(:project) do + create(:empty_project) do |project| + project.team << [current_user, :master] + end + end + + let!(:authored_issue) { create :issue, author: current_user, project: project } + let!(:authored_issue_on_public_project) { create :issue, author: current_user, project: public_project } + let!(:assigned_issue) { create :issue, assignee: current_user, project: project } + let!(:other_issue) { create :issue, project: project } + + before do + login_as(current_user) + + visit issues_dashboard_path(assignee_id: current_user.id) + end + + it 'shows issues assigned to current user' do + expect(page).to have_content(assigned_issue.title) + expect(page).not_to have_content(authored_issue.title) + expect(page).not_to have_content(other_issue.title) + end + + it 'shows issues when current user is author', js: true do + find('#assignee_id', visible: false).set('') + find('.js-author-search', match: :first).click + find('.dropdown-menu-author li a', match: :first, text: current_user.to_reference).click + + expect(page).to have_content(authored_issue.title) + expect(page).to have_content(authored_issue_on_public_project.title) + expect(page).not_to have_content(assigned_issue.title) + expect(page).not_to have_content(other_issue.title) + end + + it 'shows all issues' do + click_link('Reset filters') + + expect(page).to have_content(authored_issue.title) + expect(page).to have_content(authored_issue_on_public_project.title) + expect(page).to have_content(assigned_issue.title) + expect(page).to have_content(other_issue.title) + end +end diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb index d9be4e5dbdd3af3188c635479d1da43ad10032a7..62a2c54c94c63ad62e76c3350126f1f9d5d3588c 100644 --- a/spec/features/dashboard/shortcuts_spec.rb +++ b/spec/features/dashboard/shortcuts_spec.rb @@ -10,20 +10,20 @@ feature 'Dashboard shortcuts', feature: true, js: true do find('body').native.send_key('g') find('body').native.send_key('p') - ensure_active_main_tab('Projects') + check_page_title('Projects') find('body').native.send_key('g') find('body').native.send_key('i') - ensure_active_main_tab('Issues') + check_page_title('Issues') find('body').native.send_key('g') find('body').native.send_key('m') - ensure_active_main_tab('Merge Requests') + check_page_title('Merge Requests') end - def ensure_active_main_tab(content) - expect(find('.nav-sidebar li.active')).to have_content(content) + def check_page_title(title) + expect(find('.header-content .title')).to have_content(title) end end diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 93139dc9e94875bd31b05eb61ae23e864d3d9364..7135565294bc8f872dd4877cb7a979983540b502 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -182,6 +182,20 @@ feature 'GFM autocomplete', feature: true, js: true do expect(page).not_to have_selector('.atwho-view') end + it 'triggers autocomplete after selecting a slash command' do + note = find('#note_note') + page.within '.timeline-content-form' do + note.native.send_keys('') + note.native.send_keys('/as') + note.click + end + + find('.atwho-view li', text: '/assign').native.send_keys(:tab) + + user_item = find('.atwho-view li', text: user.username) + expect(user_item).to have_content(user.username) + end + def expect_to_wrap(should_wrap, item, note, value) expect(item).to have_content(value) expect(item).not_to have_content("\"#{value}\"") diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 755162a1eb51a2d7a4c508c33c400e4eaa2868af..094f645a077cf510b5f3b0b572308750a4f36f79 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -382,7 +382,9 @@ describe 'Issues', feature: true do it 'changes incoming email address token', js: true do find('.issue-email-modal-btn').click previous_token = find('input#issue_email').value - find('.incoming-email-token-reset').click + find('.incoming-email-token-reset').trigger('click') + + wait_for_ajax expect(page).to have_no_field('issue_email', with: previous_token) new_token = project1.new_issue_address(@user.reload) @@ -636,7 +638,7 @@ describe 'Issues', feature: true do it 'removes due date from issue' do date = Date.today.at_beginning_of_month + 2.days - + page.within '.due_date' do click_link 'Edit' diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb index a6b841c021060f2ae54a4b274e3287ef9339ef36..15c8677fcd36f29762b7343d2d956e8cd2da94b7 100644 --- a/spec/features/profiles/preferences_spec.rb +++ b/spec/features/profiles/preferences_spec.rb @@ -8,35 +8,6 @@ describe 'Profile > Preferences', feature: true do visit profile_preferences_path end - describe 'User changes their application theme', js: true do - let(:default) { Gitlab::Themes.default } - let(:theme) { Gitlab::Themes.by_id(5) } - - it 'creates a flash message' do - choose "user_theme_id_#{theme.id}" - - expect_preferences_saved_message - end - - it 'updates their preference' do - choose "user_theme_id_#{theme.id}" - - allowing_for_delay do - visit page.current_path - expect(page).to have_checked_field("user_theme_id_#{theme.id}") - end - end - - it 'reflects the changes immediately' do - expect(page).to have_selector("body.#{default.css_class}") - - choose "user_theme_id_#{theme.id}" - - expect(page).not_to have_selector("body.#{default.css_class}") - expect(page).to have_selector("body.#{theme.css_class}") - end - end - describe 'User changes their syntax highlighting theme', js: true do it 'creates a flash message' do choose 'user_color_scheme_id_5' diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index f1036b275f7cf73532a152cf9a078e3d5bfaf3fb..2116721b224691bc0dcd126b97ed73675c6cb6d7 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -275,7 +275,7 @@ feature 'Builds', :feature do let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) } let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } - it 'shows a link to lastest deployment' do + it 'shows a link to latest deployment' do visit namespace_project_build_path(project.namespace, project, build) expect(page).to have_link('latest deployment') diff --git a/spec/features/projects/main/download_buttons_spec.rb b/spec/features/projects/main/download_buttons_spec.rb index 227ccf9459c3e5728ca47bba445b3bceea13f13c..02198ff3e419f0058c796886d3c6dbabb2a65067 100644 --- a/spec/features/projects/main/download_buttons_spec.rb +++ b/spec/features/projects/main/download_buttons_spec.rb @@ -39,6 +39,13 @@ feature 'Download buttons in project main page', feature: true do expect(page).to have_link "Download '#{build.name}'", href: href end + + scenario 'download links have download attribute' do + expect(page).to have_selector('a', text: 'Download') + page.all('a', text: 'Download').each do |link| + expect(link[:download]).to eq '' + end + end end end end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 6555b2fc6c1971993a386747b39dcbd5e354dfc8..8d1214dedb4de4039d33b3d1e932406d29a65cdc 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -218,6 +218,14 @@ describe 'Pipelines', :feature, :js do expect(page).to have_link(with_artifacts.name) end + + it 'has download attribute on download links' do + find('.js-pipeline-dropdown-download').click + expect(page).to have_selector('a', text: 'Download') + page.all('.build-artifacts a', text: 'Download').each do |link| + expect(link[:download]).to eq '' + end + end end context 'with artifacts expired' do @@ -277,6 +285,27 @@ describe 'Pipelines', :feature, :js do end end end + + context 'with pagination' do + before do + allow(Ci::Pipeline).to receive(:default_per_page).and_return(1) + create(:ci_empty_pipeline, project: project) + end + + it 'should render pagination' do + visit namespace_project_pipelines_path(project.namespace, project) + wait_for_vue_resource + + expect(page).to have_selector('.gl-pagination') + end + + it 'should render second page of pipelines' do + visit namespace_project_pipelines_path(project.namespace, project, page: '2') + wait_for_vue_resource + + expect(page).to have_selector('.gl-pagination .page', count: 2) + end + end end describe 'POST /:project/pipelines' do diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 1b352be9331536e7c51d882f8c0b349852eb1ee1..fb19dac1d6af395518bbf3e524b6cdaf9887919e 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe 'Dashboard Todos', feature: true do + include WaitForAjax + let(:user) { create(:user) } let(:author) { create(:user) } let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } @@ -34,40 +36,65 @@ describe 'Dashboard Todos', feature: true do end end - describe 'deleting the todo' do + shared_examples 'deleting the todo' do before do - first('.done-todo').click + first('.js-done-todo').click + end + + it 'is marked as done-reversible in the list' do + expect(page).to have_selector('.todos-list .todo.todo-pending.done-reversible') end - it 'is removed from the list' do - expect(page).not_to have_selector('.todos-list .todo') + it 'shows Undo button' do + expect(page).to have_selector('.js-undo-todo', visible: true) + expect(page).to have_selector('.js-done-todo', visible: false) end - it 'shows "All done" message' do - expect(page).to have_selector('.todos-all-done', count: 1) + it 'updates todo count' do + expect(page).to have_content 'To do 0' + expect(page).to have_content 'Done 1' + end + + it 'has not "All done" message' do + expect(page).not_to have_selector('.todos-all-done') end end - context 'todo is stale on the page' do + shared_examples 'deleting and restoring the todo' do before do - todos = TodosFinder.new(user, state: :pending).execute - TodoService.new.mark_todos_as_done(todos, user) + first('.js-done-todo').click + wait_for_ajax + first('.js-undo-todo').click end - describe 'deleting the todo' do - before do - first('.done-todo').click - end + it 'is marked back as pending in the list' do + expect(page).not_to have_selector('.todos-list .todo.todo-pending.done-reversible') + expect(page).to have_selector('.todos-list .todo.todo-pending') + end - it 'is removed from the list' do - expect(page).not_to have_selector('.todos-list .todo') - end + it 'shows Done button' do + expect(page).to have_selector('.js-undo-todo', visible: false) + expect(page).to have_selector('.js-done-todo', visible: true) + end - it 'shows "All done" message' do - expect(page).to have_selector('.todos-all-done', count: 1) - end + it 'updates todo count' do + expect(page).to have_content 'To do 1' + expect(page).to have_content 'Done 0' end end + + it_behaves_like 'deleting the todo' + it_behaves_like 'deleting and restoring the todo' + + context 'todo is stale on the page' do + before do + todos = TodosFinder.new(user, state: :pending).execute + TodoService.new.mark_todos_as_done(todos, user) + end + + it_behaves_like 'deleting the todo' + it_behaves_like 'deleting and restoring the todo' + end end context 'User has Todos with labels spanning multiple projects' do @@ -113,18 +140,6 @@ describe 'Dashboard Todos', feature: true do expect(page).to have_selector('.gl-pagination .page', count: 2) end - describe 'completing last todo from last page', js: true do - it 'redirects to the previous page' do - visit dashboard_todos_path(page: 2) - expect(page).to have_css("#todo_#{Todo.last.id}") - - click_link('Done') - - expect(current_path).to eq dashboard_todos_path - expect(page).to have_css("#todo_#{Todo.first.id}") - end - end - describe 'mark all as done', js: true do before do visit dashboard_todos_path diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 12ab1d6dde8279a11132bd99c4ee6620c5573926..2a00842747872fb0157712325dddb6dfc69dbdd0 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -136,10 +136,10 @@ describe IssuesFinder do end end - context 'filtering by issue iid' do - let(:params) { { search: issue3.to_reference } } + context 'filtering by issues iids' do + let(:params) { { iids: issue3.iid } } - it 'returns issue with iid match' do + it 'returns issues with iids match' do expect(issues).to contain_exactly(issue3) end end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 3dcd7781e5b73d08b3ae349c8d2103007a78f61b..21ef94ac5d1873c7ddefab851b362f31bcd72534 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -38,5 +38,13 @@ describe MergeRequestsFinder do merge_requests = MergeRequestsFinder.new(user, params).execute expect(merge_requests.size).to eq(3) end + + it 'filters by iid' do + params = { project_id: project1.id, iids: merge_request1.iid } + + merge_requests = MergeRequestsFinder.new(user, params).execute + + expect(merge_requests).to contain_exactly(merge_request1) + end end end diff --git a/spec/fixtures/api/schemas/user/login.json b/spec/fixtures/api/schemas/user/login.json index e6c1d9c9d845ab527f03a768f4c98a60ace92d20..6181b3ccc8694545c49994cacfc80002688c4fbe 100644 --- a/spec/fixtures/api/schemas/user/login.json +++ b/spec/fixtures/api/schemas/user/login.json @@ -19,7 +19,6 @@ "organization", "last_sign_in_at", "confirmed_at", - "theme_id", "color_scheme_id", "projects_limit", "current_sign_in_at", diff --git a/spec/fixtures/api/schemas/user/public.json b/spec/fixtures/api/schemas/user/public.json index dbd5d32e89cdaee608bc9634b388780398808e93..5587cfec61ac0acf4e1c8b74dcd1e03192e64239 100644 --- a/spec/fixtures/api/schemas/user/public.json +++ b/spec/fixtures/api/schemas/user/public.json @@ -19,7 +19,6 @@ "organization", "last_sign_in_at", "confirmed_at", - "theme_id", "color_scheme_id", "projects_limit", "current_sign_in_at", @@ -32,14 +31,14 @@ "properties": { "id": { "type": "integer" }, "username": { "type": "string" }, - "email": { + "email": { "type": "string", "pattern": "^[^@]+@[^@]+$" }, "name": { "type": "string" }, - "state": { + "state": { "type": "string", - "enum": ["active", "blocked"] + "enum": ["active", "blocked"] }, "avatar_url": { "type": "string" }, "web_url": { "type": "string" }, @@ -54,18 +53,17 @@ "organization": { "type": ["string", "null"] }, "last_sign_in_at": { "type": "date" }, "confirmed_at": { "type": ["date", "null"] }, - "theme_id": { "type": "integer" }, "color_scheme_id": { "type": "integer" }, "projects_limit": { "type": "integer" }, "current_sign_in_at": { "type": "date" }, - "identities": { + "identities": { "type": "array", "items": { "type": "object", "properties": { - "provider": { + "provider": { "type": "string", - "enum": ["github", "bitbucket", "google_oauth2"] + "enum": ["github", "bitbucket", "google_oauth2"] }, "extern_uid": { "type": ["number", "string"] } } @@ -74,6 +72,6 @@ "can_create_group": { "type": "boolean" }, "can_create_project": { "type": "boolean" }, "two_factor_enabled": { "type": "boolean" }, - "external": { "type": "boolean" } + "external": { "type": "boolean" } } } diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 8b201f348f19d2fc181b3b5544e52df2954ebee7..fd40fe999411f3e4a862f20e673f0f5d3cf58a9a 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -265,4 +265,9 @@ describe ApplicationHelper do expect(helper.render_markup('foo.adoc', content)).to eq('NOEL') end end + + describe '#active_when' do + it { expect(helper.active_when(true)).to eq('active') } + it { expect(helper.active_when(false)).to eq(nil) } + end end diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb index dc07657e101354d98fd46db4c59776f18656dfb9..2cc0b40b2d052e270c830640599afe05d1dfcd9d 100644 --- a/spec/helpers/page_layout_helper_spec.rb +++ b/spec/helpers/page_layout_helper_spec.rb @@ -40,6 +40,18 @@ describe PageLayoutHelper do end end + describe 'favicon' do + it 'defaults to favicon.ico' do + allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production')) + expect(helper.favicon).to eq 'favicon.ico' + end + + it 'has blue favicon for development' do + allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development')) + expect(helper.favicon).to eq 'favicon-blue.ico' + end + end + describe 'page_image' do it 'defaults to the GitLab logo' do expect(helper.page_image).to end_with 'assets/gitlab_logo.png' diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index 1f02e06e312580890e222cced2e9dce09052c696..f3e79cc729017a6cf91eef8deb4c2e43736c7651 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -26,32 +26,6 @@ describe PreferencesHelper do end end - describe 'user_application_theme' do - context 'with a user' do - it "returns user's theme's css_class" do - stub_user(theme_id: 3) - - expect(helper.user_application_theme).to eq 'ui_green' - end - - it 'returns the default when id is invalid' do - stub_user(theme_id: Gitlab::Themes.count + 5) - - allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(2) - - expect(helper.user_application_theme).to eq 'ui_charcoal' - end - end - - context 'without a user' do - it 'returns the default theme' do - stub_user - - expect(helper.user_application_theme).to eq Gitlab::Themes.default.css_class - end - end - end - describe 'user_color_scheme' do context 'with a user' do it "returns user's scheme's css_class" do diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 001cd8d6325a67a4ae95832ebdb24522e67d6fe4..e5826f9c29f70a4cb8ff196115191807d59eac45 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -229,4 +229,4 @@ require('./fixtures/emoji_menu'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js index 4a3da9e318b9987d479e57b2027a28d5350c94c0..3deaf258caedfe31a81c8b99ce846c72695aad38 100644 --- a/spec/javascripts/behaviors/autosize_spec.js +++ b/spec/javascripts/behaviors/autosize_spec.js @@ -18,4 +18,4 @@ require('~/behaviors/autosize'); return $(document).trigger('load'); }; }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index 0e4c2c560cc44741172f8fa1e1dfb64cbd391556..4820ce41ade3869e088880d577a96a56d162da49 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -103,4 +103,4 @@ require('~/behaviors/quick_submit'); return $.Event('keydown', $.extend({}, defaults, options)); }; }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index 631fca0651499737c9dda337f5267788a7a08010..3a84013a2ed9dfe5f98d60c8a927978b919182d5 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -36,4 +36,4 @@ require('~/behaviors/requires_input'); return expect($('.submit')).not.toBeDisabled(); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 index 789f5dc9f49e6188a408b1bd456f6b9b5ac425da..949734199790bd4c331cce8288de99f29d7640db 100644 --- a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 +++ b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 @@ -1,10 +1,10 @@ -require('~/commit/pipelines/pipelines_store'); +const PipelinesStore = require('~/commit/pipelines/pipelines_store'); describe('Store', () => { let store; beforeEach(() => { - store = new gl.commits.pipelines.PipelinesStore(); + store = new PipelinesStore(); }); // unregister intervals and event handlers diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6 deleted file mode 100644 index c0bdb89ed63b2858966dfd1186c29cdda2ed7f5e..0000000000000000000000000000000000000000 --- a/spec/javascripts/dashboard_spec.js.es6 +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable no-new */ - -require('~/sidebar'); -require('~/lib/utils/text_utility'); - -((global) => { - describe('Dashboard', () => { - const fixtureTemplate = 'static/dashboard.html.raw'; - - function todosCountText() { - return $('.js-todos-count').text(); - } - - function triggerToggle(newCount) { - $(document).trigger('todo:toggle', newCount); - } - - preloadFixtures(fixtureTemplate); - beforeEach(() => { - loadFixtures(fixtureTemplate); - new global.Sidebar(); - }); - - it('should update todos-count after receiving the todo:toggle event', () => { - triggerToggle(5); - expect(todosCountText()).toEqual('5'); - }); - - it('should display todos-count with delimiter', () => { - triggerToggle(1000); - expect(todosCountText()).toEqual('1,000'); - - triggerToggle(1000000); - expect(todosCountText()).toEqual('1,000,000'); - }); - }); -})(window.gl); diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6 index b1838045a06d2e74ff1d1dac656777a0589f43fa..850586f9f3acd964e45a86440ecf92fc5b9d969e 100644 --- a/spec/javascripts/environments/environment_actions_spec.js.es6 +++ b/spec/javascripts/environments/environment_actions_spec.js.es6 @@ -1,4 +1,4 @@ -require('~/environments/components/environment_actions'); +const ActionsComponent = require('~/environments/components/environment_actions'); describe('Actions Component', () => { preloadFixtures('static/environments/element.html.raw'); @@ -19,7 +19,7 @@ describe('Actions Component', () => { }, ]; - const component = new window.gl.environmentsList.ActionsComponent({ + const component = new ActionsComponent({ el: document.querySelector('.test-dom-element'), propsData: { actions: actionsMock, @@ -47,7 +47,7 @@ describe('Actions Component', () => { }, ]; - const component = new window.gl.environmentsList.ActionsComponent({ + const component = new ActionsComponent({ el: document.querySelector('.test-dom-element'), propsData: { actions: actionsMock, diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6 index a6a587e69f5909a778f8f20d31bbb2c189042340..393dbb5aae08be420c8ab879377e01f75c0bc2f7 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js.es6 +++ b/spec/javascripts/environments/environment_external_url_spec.js.es6 @@ -1,4 +1,4 @@ -require('~/environments/components/environment_external_url'); +const ExternalUrlComponent = require('~/environments/components/environment_external_url'); describe('External URL Component', () => { preloadFixtures('static/environments/element.html.raw'); @@ -8,7 +8,7 @@ describe('External URL Component', () => { it('should link to the provided externalUrl prop', () => { const externalURL = 'https://gitlab.com'; - const component = new window.gl.environmentsList.ExternalUrlComponent({ + const component = new ExternalUrlComponent({ el: document.querySelector('.test-dom-element'), propsData: { externalUrl: externalURL, diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index e6fb0d0029066811c85c9f35944a3b03a96ca7d1..7fea80ed799e579048f30b3439931314ba6e785f 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -1,5 +1,5 @@ window.timeago = require('timeago.js'); -require('~/environments/components/environment_item'); +const EnvironmentItem = require('~/environments/components/environment_item'); describe('Environment item', () => { preloadFixtures('static/environments/table.html.raw'); @@ -14,33 +14,16 @@ describe('Environment item', () => { beforeEach(() => { mockItem = { name: 'review', - children: [ - { - name: 'review-app', - id: 1, - state: 'available', - external_url: '', - last_deployment: {}, - created_at: '2016-11-07T11:11:16.525Z', - updated_at: '2016-11-10T15:55:58.778Z', - }, - { - name: 'production', - id: 2, - state: 'available', - external_url: '', - last_deployment: {}, - created_at: '2016-11-07T11:11:16.525Z', - updated_at: '2016-11-10T15:55:58.778Z', - }, - ], + folderName: 'review', + size: 3, + isFolder: true, + environment_path: 'url', }; - component = new window.gl.environmentsList.EnvironmentItem({ + component = new EnvironmentItem({ el: document.querySelector('tr#environment-row'), propsData: { model: mockItem, - toggleRow: () => {}, canCreateDeployment: false, canReadEnvironment: true, }, @@ -53,7 +36,7 @@ describe('Environment item', () => { }); it('Should render the number of children in a badge', () => { - expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.children.length); + expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.size); }); }); @@ -63,8 +46,8 @@ describe('Environment item', () => { beforeEach(() => { environment = { - id: 31, name: 'production', + size: 1, state: 'stopped', external_url: 'http://external.com', environment_type: null, @@ -125,11 +108,10 @@ describe('Environment item', () => { updated_at: '2016-11-10T15:55:58.778Z', }; - component = new window.gl.environmentsList.EnvironmentItem({ + component = new EnvironmentItem({ el: document.querySelector('tr#environment-row'), propsData: { model: environment, - toggleRow: () => {}, canCreateDeployment: true, canReadEnvironment: true, }, diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6 index 043b8708a6ec042730be08900baa0ce9e5a7f64b..4a596baad09cfdf52157b11626cfc6945d5d46c9 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js.es6 +++ b/spec/javascripts/environments/environment_rollback_spec.js.es6 @@ -1,4 +1,4 @@ -require('~/environments/components/environment_rollback'); +const RollbackComponent = require('~/environments/components/environment_rollback'); describe('Rollback Component', () => { preloadFixtures('static/environments/element.html.raw'); @@ -10,7 +10,7 @@ describe('Rollback Component', () => { }); it('Should link to the provided retryUrl', () => { - const component = new window.gl.environmentsList.RollbackComponent({ + const component = new RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { retryUrl: retryURL, @@ -22,7 +22,7 @@ describe('Rollback Component', () => { }); it('Should render Re-deploy label when isLastDeployment is true', () => { - const component = new window.gl.environmentsList.RollbackComponent({ + const component = new RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { retryUrl: retryURL, @@ -34,7 +34,7 @@ describe('Rollback Component', () => { }); it('Should render Rollback label when isLastDeployment is false', () => { - const component = new window.gl.environmentsList.RollbackComponent({ + const component = new RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { retryUrl: retryURL, diff --git a/spec/javascripts/environments/environment_spec.js.es6 b/spec/javascripts/environments/environment_spec.js.es6 index 87eda136122de0db57de10c89e02fe01a7c1336d..edd0cad32d08afaad849e021aef3537b7b291e90 100644 --- a/spec/javascripts/environments/environment_spec.js.es6 +++ b/spec/javascripts/environments/environment_spec.js.es6 @@ -1,9 +1,7 @@ -/* global Vue, environment */ - +const Vue = require('vue'); require('~/flash'); -require('~/environments/stores/environments_store'); -require('~/environments/components/environment'); -require('./mock_data'); +const EnvironmentsComponent = require('~/environments/components/environment'); +const { environment } = require('./mock_data'); describe('Environment', () => { preloadFixtures('static/environments/environments.html.raw'); @@ -33,11 +31,8 @@ describe('Environment', () => { }); it('should render the empty state', (done) => { - component = new gl.environmentsList.EnvironmentsComponent({ + component = new EnvironmentsComponent({ el: document.querySelector('#environments-list-view'), - propsData: { - store: gl.environmentsList.EnvironmentsStore.create(), - }, }); setTimeout(() => { @@ -54,15 +49,30 @@ describe('Environment', () => { }); }); - describe('with environments', () => { + describe('with paginated environments', () => { const environmentsResponseInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([environment]), { + next(request.respondWith(JSON.stringify({ + environments: [environment], + stopped_count: 1, + available_count: 0, + }), { status: 200, + headers: { + 'X-nExt-pAge': '2', + 'x-page': '1', + 'X-Per-Page': '1', + 'X-Prev-Page': '', + 'X-TOTAL': '37', + 'X-Total-Pages': '2', + }, })); }; beforeEach(() => { Vue.http.interceptors.push(environmentsResponseInterceptor); + component = new EnvironmentsComponent({ + el: document.querySelector('#environments-list-view'), + }); }); afterEach(() => { @@ -72,13 +82,6 @@ describe('Environment', () => { }); it('should render a table with environments', (done) => { - component = new gl.environmentsList.EnvironmentsComponent({ - el: document.querySelector('#environments-list-view'), - propsData: { - store: gl.environmentsList.EnvironmentsStore.create(), - }, - }); - setTimeout(() => { expect( component.$el.querySelectorAll('table tbody tr').length, @@ -86,6 +89,59 @@ describe('Environment', () => { done(); }, 0); }); + + describe('pagination', () => { + it('should render pagination', (done) => { + setTimeout(() => { + expect( + component.$el.querySelectorAll('.gl-pagination li').length, + ).toEqual(5); + done(); + }, 0); + }); + + it('should update url when no search params are present', (done) => { + spyOn(gl.utils, 'visitUrl'); + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2'); + done(); + }, 0); + }); + + it('should update url when page is already present', (done) => { + spyOn(gl.utils, 'visitUrl'); + window.history.pushState({}, null, '?page=1'); + + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2'); + done(); + }, 0); + }); + + it('should update url when page and scope are already present', (done) => { + spyOn(gl.utils, 'visitUrl'); + window.history.pushState({}, null, '?scope=all&page=1'); + + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2'); + done(); + }, 0); + }); + + it('should update url when page and scope are already present and page is first param', (done) => { + spyOn(gl.utils, 'visitUrl'); + window.history.pushState({}, null, '?page=1&scope=all'); + + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all'); + done(); + }, 0); + }); + }); }); }); @@ -107,11 +163,8 @@ describe('Environment', () => { }); it('should render empty state', (done) => { - component = new gl.environmentsList.EnvironmentsComponent({ + component = new EnvironmentsComponent({ el: document.querySelector('#environments-list-view'), - propsData: { - store: gl.environmentsList.EnvironmentsStore.create(), - }, }); setTimeout(() => { diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6 index 2dfce5ba824bb0f461ead921c8fe36411213b471..5ca65b1debc32e61f817929ca812b1b352dd9262 100644 --- a/spec/javascripts/environments/environment_stop_spec.js.es6 +++ b/spec/javascripts/environments/environment_stop_spec.js.es6 @@ -1,4 +1,4 @@ -require('~/environments/components/environment_stop'); +const StopComponent = require('~/environments/components/environment_stop'); describe('Stop Component', () => { preloadFixtures('static/environments/element.html.raw'); @@ -10,7 +10,7 @@ describe('Stop Component', () => { loadFixtures('static/environments/element.html.raw'); stopURL = '/stop'; - component = new window.gl.environmentsList.StopComponent({ + component = new StopComponent({ el: document.querySelector('.test-dom-element'), propsData: { stopUrl: stopURL, diff --git a/spec/javascripts/environments/environment_table_spec.js.es6 b/spec/javascripts/environments/environment_table_spec.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..be4330b50124a9e87ae16fa871ef4f8728d85b89 --- /dev/null +++ b/spec/javascripts/environments/environment_table_spec.js.es6 @@ -0,0 +1,30 @@ +const EnvironmentTable = require('~/environments/components/environments_table'); + +describe('Environment item', () => { + preloadFixtures('static/environments/element.html.raw'); + beforeEach(() => { + loadFixtures('static/environments/element.html.raw'); + }); + + it('Should render a table', () => { + const mockItem = { + name: 'review', + size: 3, + isFolder: true, + latest: { + environment_path: 'url', + }, + }; + + const component = new EnvironmentTable({ + el: document.querySelector('.test-dom-element'), + propsData: { + environments: [{ mockItem }], + canCreateDeployment: false, + canReadEnvironment: true, + }, + }); + + expect(component.$el.tagName).toEqual('TABLE'); + }); +}); diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index 9a8300d3832645ddf9e1e0b4159e7d3d04209537..77e182b3830781efb874e687da73db79e3c94282 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -1,70 +1,58 @@ -/* global environmentsList */ - -require('~/environments/stores/environments_store'); -require('./mock_data'); +const Store = require('~/environments/stores/environments_store'); +const { environmentsList, serverData } = require('./mock_data'); (() => { describe('Store', () => { + let store; + beforeEach(() => { - gl.environmentsList.EnvironmentsStore.create(); + store = new Store(); }); it('should start with a blank state', () => { - expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(0); - expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(0); - expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(0); + expect(store.state.environments.length).toEqual(0); + expect(store.state.stoppedCounter).toEqual(0); + expect(store.state.availableCounter).toEqual(0); + expect(store.state.paginationInformation).toEqual({}); }); - describe('store environments', () => { - beforeEach(() => { - gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList); - }); - - it('should count stopped environments and save the count in the state', () => { - expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(1); - }); - - it('should count available environments and save the count in the state', () => { - expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(3); - }); - - it('should store environments with same environment_type as sibilings', () => { - expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(3); - - const parentFolder = gl.environmentsList.EnvironmentsStore.state.environments - .filter(env => env.children && env.children.length > 0); - - expect(parentFolder[0].children.length).toBe(2); - expect(parentFolder[0].children[0].environment_type).toBe('review'); - expect(parentFolder[0].children[1].environment_type).toBe('review'); - expect(parentFolder[0].children[0].name).toBe('test-environment'); - expect(parentFolder[0].children[1].name).toBe('test-environment-1'); - }); - - it('should sort the environments alphabetically', () => { - const { environments } = gl.environmentsList.EnvironmentsStore.state; - - expect(environments[0].name).toBe('production'); - expect(environments[1].name).toBe('review'); - expect(environments[1].children[0].name).toBe('test-environment'); - expect(environments[1].children[1].name).toBe('test-environment-1'); - expect(environments[2].name).toBe('review_app'); - }); + it('should store environments', () => { + store.storeEnvironments(serverData); + expect(store.state.environments.length).toEqual(serverData.length); + expect(store.state.environments[0]).toEqual(environmentsList[0]); }); - describe('toggleFolder', () => { - beforeEach(() => { - gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList); - }); - - it('should toggle the open property for the given environment', () => { - gl.environmentsList.EnvironmentsStore.toggleFolder('review'); + it('should store available count', () => { + store.storeAvailableCount(2); + expect(store.state.availableCounter).toEqual(2); + }); - const { environments } = gl.environmentsList.EnvironmentsStore.state; - const environment = environments.filter(env => env['vue-isChildren'] === true && env.name === 'review'); + it('should store stopped count', () => { + store.storeStoppedCount(2); + expect(store.state.stoppedCounter).toEqual(2); + }); - expect(environment[0].isOpen).toBe(true); - }); + it('should store pagination information', () => { + const pagination = { + 'X-nExt-pAge': '2', + 'X-page': '1', + 'X-Per-Page': '1', + 'X-Prev-Page': '2', + 'X-TOTAL': '37', + 'X-Total-Pages': '2', + }; + + const expectedResult = { + perPage: 1, + page: 1, + total: 37, + totalPages: 2, + nextPage: 2, + previousPage: 2, + }; + + store.setPagination(pagination); + expect(store.state.paginationInformation).toEqual(expectedResult); }); }); })(); diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6 b/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..d1335b5b3040da4c78500d601ca45dac0766accc --- /dev/null +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6 @@ -0,0 +1,202 @@ +const Vue = require('vue'); +require('~/flash'); +const EnvironmentsFolderViewComponent = require('~/environments/folder/environments_folder_view'); +const { environmentsList } = require('../mock_data'); + +describe('Environments Folder View', () => { + preloadFixtures('static/environments/environments_folder_view.html.raw'); + + beforeEach(() => { + loadFixtures('static/environments/environments_folder_view.html.raw'); + window.history.pushState({}, null, 'environments/folders/build'); + }); + + let component; + + describe('successfull request', () => { + const environmentsResponseInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify({ + environments: environmentsList, + stopped_count: 1, + available_count: 0, + }), { + status: 200, + headers: { + 'X-nExt-pAge': '2', + 'x-page': '1', + 'X-Per-Page': '1', + 'X-Prev-Page': '', + 'X-TOTAL': '37', + 'X-Total-Pages': '2', + }, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(environmentsResponseInterceptor); + component = new EnvironmentsFolderViewComponent({ + el: document.querySelector('#environments-folder-list-view'), + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, environmentsResponseInterceptor, + ); + }); + + it('should render a table with environments', (done) => { + setTimeout(() => { + expect( + component.$el.querySelectorAll('table tbody tr').length, + ).toEqual(2); + done(); + }, 0); + }); + + it('should render available tab with count', (done) => { + setTimeout(() => { + expect( + component.$el.querySelector('.js-available-environments-folder-tab').textContent, + ).toContain('Available'); + + expect( + component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent, + ).toContain('0'); + done(); + }, 0); + }); + + it('should render stopped tab with count', (done) => { + setTimeout(() => { + expect( + component.$el.querySelector('.js-stopped-environments-folder-tab').textContent, + ).toContain('Stopped'); + + expect( + component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent, + ).toContain('1'); + done(); + }, 0); + }); + + it('should render parent folder name', (done) => { + setTimeout(() => { + expect( + component.$el.querySelector('.js-folder-name').textContent, + ).toContain('Environments / build'); + done(); + }, 0); + }); + + describe('pagination', () => { + it('should render pagination', (done) => { + setTimeout(() => { + expect( + component.$el.querySelectorAll('.gl-pagination li').length, + ).toEqual(5); + done(); + }, 0); + }); + + it('should update url when no search params are present', (done) => { + spyOn(gl.utils, 'visitUrl'); + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2'); + done(); + }, 0); + }); + + it('should update url when page is already present', (done) => { + spyOn(gl.utils, 'visitUrl'); + window.history.pushState({}, null, '?page=1'); + + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2'); + done(); + }, 0); + }); + + it('should update url when page and scope are already present', (done) => { + spyOn(gl.utils, 'visitUrl'); + window.history.pushState({}, null, '?scope=all&page=1'); + + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2'); + done(); + }, 0); + }); + + it('should update url when page and scope are already present and page is first param', (done) => { + spyOn(gl.utils, 'visitUrl'); + window.history.pushState({}, null, '?page=1&scope=all'); + + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all'); + done(); + }, 0); + }); + }); + }); + + describe('unsuccessfull request', () => { + const environmentsErrorResponseInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 500, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(environmentsErrorResponseInterceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, environmentsErrorResponseInterceptor, + ); + }); + + it('should not render a table', (done) => { + component = new EnvironmentsFolderViewComponent({ + el: document.querySelector('#environments-folder-list-view'), + }); + + setTimeout(() => { + expect( + component.$el.querySelector('table'), + ).toBe(null); + done(); + }, 0); + }); + + it('should render available tab with count 0', (done) => { + setTimeout(() => { + expect( + component.$el.querySelector('.js-available-environments-folder-tab').textContent, + ).toContain('Available'); + + expect( + component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent, + ).toContain('0'); + done(); + }, 0); + }); + + it('should render stopped tab with count 0', (done) => { + setTimeout(() => { + expect( + component.$el.querySelector('.js-stopped-environments-folder-tab').textContent, + ).toContain('Stopped'); + + expect( + component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent, + ).toContain('0'); + done(); + }, 0); + }); + }); +}); diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6 index 80e1cbc6f4df7f5ccdee2eb464644dc7282a936b..5c395c6b2d89b733ffc73a5f06f4c81764ec775a 100644 --- a/spec/javascripts/environments/mock_data.js.es6 +++ b/spec/javascripts/environments/mock_data.js.es6 @@ -1,153 +1,92 @@ - const environmentsList = [ { - id: 31, - name: 'production', + name: 'DEV', + size: 1, + id: 7, state: 'available', - external_url: 'https://www.gitlab.com', - environment_type: null, - last_deployment: { - id: 64, - iid: 5, - sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - ref: { - name: 'master', - ref_url: 'http://localhost:3000/root/ci-folders/tree/master', - }, - tag: false, - 'last?': true, - user: { - name: 'Administrator', - username: 'root', - id: 1, - state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', - web_url: 'http://localhost:3000/root', - }, - commit: { - id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - short_id: '500aabcb', - title: 'Update .gitlab-ci.yml', - author_name: 'Administrator', - author_email: 'admin@example.com', - created_at: '2016-11-07T18:28:13.000+00:00', - message: 'Update .gitlab-ci.yml', - author: { - name: 'Administrator', - username: 'root', - id: 1, - state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', - web_url: 'http://localhost:3000/root', - }, - commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - }, - deployable: { - id: 1278, - name: 'build', - build_path: '/root/ci-folders/builds/1278', - retry_path: '/root/ci-folders/builds/1278/retry', - }, - manual_actions: [], - }, - 'stop_action?': true, - environment_path: '/root/ci-folders/environments/31', - created_at: '2016-11-07T11:11:16.525Z', - updated_at: '2016-11-07T11:11:16.525Z', - }, - { - id: 32, - name: 'review_app', - state: 'stopped', - external_url: 'https://www.gitlab.com', + external_url: null, environment_type: null, - last_deployment: { - id: 64, - iid: 5, - sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - ref: { - name: 'master', - ref_url: 'http://localhost:3000/root/ci-folders/tree/master', - }, - tag: false, - 'last?': true, - user: { - name: 'Administrator', - username: 'root', - id: 1, - state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', - web_url: 'http://localhost:3000/root', - }, - commit: { - id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - short_id: '500aabcb', - title: 'Update .gitlab-ci.yml', - author_name: 'Administrator', - author_email: 'admin@example.com', - created_at: '2016-11-07T18:28:13.000+00:00', - message: 'Update .gitlab-ci.yml', - author: { - name: 'Administrator', - username: 'root', - id: 1, - state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', - web_url: 'http://localhost:3000/root', - }, - commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - }, - deployable: { - id: 1278, - name: 'build', - build_path: '/root/ci-folders/builds/1278', - retry_path: '/root/ci-folders/builds/1278/retry', - }, - manual_actions: [], - }, + last_deployment: null, 'stop_action?': false, - environment_path: '/root/ci-folders/environments/31', - created_at: '2016-11-07T11:11:16.525Z', - updated_at: '2016-11-07T11:11:16.525Z', + environment_path: '/root/review-app/environments/7', + stop_path: '/root/review-app/environments/7/stop', + created_at: '2017-01-31T10:53:46.894Z', + updated_at: '2017-01-31T10:53:46.894Z', }, { - id: 33, - name: 'test-environment', + folderName: 'build', + size: 5, + id: 12, + name: 'build/update-README', state: 'available', - environment_type: 'review', + external_url: null, + environment_type: 'build', last_deployment: null, - 'stop_action?': true, - environment_path: '/root/ci-folders/environments/31', - created_at: '2016-11-07T11:11:16.525Z', - updated_at: '2016-11-07T11:11:16.525Z', + 'stop_action?': false, + environment_path: '/root/review-app/environments/12', + stop_path: '/root/review-app/environments/12/stop', + created_at: '2017-02-01T19:42:18.400Z', + updated_at: '2017-02-01T19:42:18.400Z', }, +]; + +const serverData = [ { - id: 34, - name: 'test-environment-1', - state: 'available', - environment_type: 'review', - last_deployment: null, - 'stop_action?': true, - environment_path: '/root/ci-folders/environments/31', - created_at: '2016-11-07T11:11:16.525Z', - updated_at: '2016-11-07T11:11:16.525Z', + name: 'DEV', + size: 1, + latest: { + id: 7, + name: 'DEV', + state: 'available', + external_url: null, + environment_type: null, + last_deployment: null, + 'stop_action?': false, + environment_path: '/root/review-app/environments/7', + stop_path: '/root/review-app/environments/7/stop', + created_at: '2017-01-31T10:53:46.894Z', + updated_at: '2017-01-31T10:53:46.894Z', + }, + }, + { + name: 'build', + size: 5, + latest: { + id: 12, + name: 'build/update-README', + state: 'available', + external_url: null, + environment_type: 'build', + last_deployment: null, + 'stop_action?': false, + environment_path: '/root/review-app/environments/12', + stop_path: '/root/review-app/environments/12/stop', + created_at: '2017-02-01T19:42:18.400Z', + updated_at: '2017-02-01T19:42:18.400Z', + }, }, ]; -window.environmentsList = environmentsList; - const environment = { - id: 4, - name: 'production', - state: 'available', - external_url: 'http://production.', - environment_type: null, - last_deployment: {}, - 'stop_action?': false, - environment_path: '/root/review-app/environments/4', - stop_path: '/root/review-app/environments/4/stop', - created_at: '2016-12-16T11:51:04.690Z', - updated_at: '2016-12-16T12:04:51.133Z', + name: 'DEV', + size: 1, + latest: { + id: 7, + name: 'DEV', + state: 'available', + external_url: null, + environment_type: null, + last_deployment: null, + 'stop_action?': false, + environment_path: '/root/review-app/environments/7', + stop_path: '/root/review-app/environments/7/stop', + created_at: '2017-01-31T10:53:46.894Z', + updated_at: '2017-01-31T10:53:46.894Z', + }, }; -window.environment = environment; +module.exports = { + environmentsList, + environment, + serverData, +}; diff --git a/spec/javascripts/extensions/array_spec.js.es6 b/spec/javascripts/extensions/array_spec.js.es6 index ba5eb81defcac5eb187f804abdc2edcfcb5f8662..60f6b9b78e3b77f9fd09feefaf991b3cf13891a2 100644 --- a/spec/javascripts/extensions/array_spec.js.es6 +++ b/spec/javascripts/extensions/array_spec.js.es6 @@ -42,4 +42,4 @@ require('~/extensions/array'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js index c0bb04198146120ff9aee2b1d31b392428a288c7..096d3272eac164a1ac87f6906b4bce438832a060 100644 --- a/spec/javascripts/extensions/jquery_spec.js +++ b/spec/javascripts/extensions/jquery_spec.js @@ -39,4 +39,4 @@ require('~/extensions/jquery'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/fixtures/dashboard.html.haml b/spec/javascripts/fixtures/dashboard.html.haml deleted file mode 100644 index 32446acfd60aa67dda707d245dafbf51eb066b3a..0000000000000000000000000000000000000000 --- a/spec/javascripts/fixtures/dashboard.html.haml +++ /dev/null @@ -1,45 +0,0 @@ -%ul.nav.nav-sidebar - %li.home.active - %a.dashboard-shortcuts-projects - %span - Projects - %li - %a - %span - Todos - %span.count.js-todos-count - 1 - %li - %a.dashboard-shortcuts-activity - %span - Activity - %li - %a - %span - Groups - %li - %a - %span - Milestones - %li - %a.dashboard-shortcuts-issues - %span - Issues - %span - 1 - %li - %a.dashboard-shortcuts-merge_requests - %span - Merge Requests - %li - %a - %span - Snippets - %li - %a - %span - Help - %li - %a - %span - Profile Settings diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js index 2ef242901e8125dc8e0e763a01eb97a3938c38eb..a50812d9517bc3c2cdad724f925d0cb788289deb 100644 --- a/spec/javascripts/fixtures/emoji_menu.js +++ b/spec/javascripts/fixtures/emoji_menu.js @@ -1,4 +1,4 @@ /* eslint-disable space-before-function-paren */ (function() { window.emojiMenu = "<div class='emoji-menu'>\n <input type=\"text\" name=\"emoji_search\" id=\"emoji_search\" value=\"\" class=\"emoji-search search-input form-control\" />\n <div class='emoji-menu-content'>\n <h5 class='emoji-menu-title'>\n Emoticons\n </h5>\n <ul class='clearfix emoji-menu-list'>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47D\" title=\"alien\" data-aliases=\"\" data-emoji=\"alien\" data-unicode-name=\"1F47D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47C\" title=\"angel\" data-aliases=\"\" data-emoji=\"angel\" data-unicode-name=\"1F47C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A2\" title=\"anger\" data-aliases=\"\" data-emoji=\"anger\" data-unicode-name=\"1F4A2\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F620\" title=\"angry\" data-aliases=\"\" data-emoji=\"angry\" data-unicode-name=\"1F620\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F627\" title=\"anguished\" data-aliases=\"\" data-emoji=\"anguished\" data-unicode-name=\"1F627\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F632\" title=\"astonished\" data-aliases=\"\" data-emoji=\"astonished\" data-unicode-name=\"1F632\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45F\" title=\"athletic_shoe\" data-aliases=\"\" data-emoji=\"athletic_shoe\" data-unicode-name=\"1F45F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F476\" title=\"baby\" data-aliases=\"\" data-emoji=\"baby\" data-unicode-name=\"1F476\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F459\" title=\"bikini\" data-aliases=\"\" data-emoji=\"bikini\" data-unicode-name=\"1F459\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F499\" title=\"blue_heart\" data-aliases=\"\" data-emoji=\"blue_heart\" data-unicode-name=\"1F499\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60A\" title=\"blush\" data-aliases=\"\" data-emoji=\"blush\" data-unicode-name=\"1F60A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A5\" title=\"boom\" data-aliases=\"\" data-emoji=\"boom\" data-unicode-name=\"1F4A5\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F462\" title=\"boot\" data-aliases=\"\" data-emoji=\"boot\" data-unicode-name=\"1F462\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F647\" title=\"bow\" data-aliases=\"\" data-emoji=\"bow\" data-unicode-name=\"1F647\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F466\" title=\"boy\" data-aliases=\"\" data-emoji=\"boy\" data-unicode-name=\"1F466\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F470\" title=\"bride_with_veil\" data-aliases=\"\" data-emoji=\"bride_with_veil\" data-unicode-name=\"1F470\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4BC\" title=\"briefcase\" data-aliases=\"\" data-emoji=\"briefcase\" data-unicode-name=\"1F4BC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F494\" title=\"broken_heart\" data-aliases=\"\" data-emoji=\"broken_heart\" data-unicode-name=\"1F494\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F464\" title=\"bust_in_silhouette\" data-aliases=\"\" data-emoji=\"bust_in_silhouette\" data-unicode-name=\"1F464\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F465\" title=\"busts_in_silhouette\" data-aliases=\"\" data-emoji=\"busts_in_silhouette\" data-unicode-name=\"1F465\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44F\" title=\"clap\" data-aliases=\"\" data-emoji=\"clap\" data-unicode-name=\"1F44F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F302\" title=\"closed_umbrella\" data-aliases=\"\" data-emoji=\"closed_umbrella\" data-unicode-name=\"1F302\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F630\" title=\"cold_sweat\" data-aliases=\"\" data-emoji=\"cold_sweat\" data-unicode-name=\"1F630\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F616\" title=\"confounded\" data-aliases=\"\" data-emoji=\"confounded\" data-unicode-name=\"1F616\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F615\" title=\"confused\" data-aliases=\"\" data-emoji=\"confused\" data-unicode-name=\"1F615\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F477\" title=\"construction_worker\" data-aliases=\"\" data-emoji=\"construction_worker\" data-unicode-name=\"1F477\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46E\" title=\"cop\" data-aliases=\"\" data-emoji=\"cop\" data-unicode-name=\"1F46E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46B\" title=\"couple\" data-aliases=\"\" data-emoji=\"couple\" data-unicode-name=\"1F46B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F491\" title=\"couple_with_heart\" data-aliases=\"\" data-emoji=\"couple_with_heart\" data-unicode-name=\"1F491\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48F\" title=\"couplekiss\" data-aliases=\"\" data-emoji=\"couplekiss\" data-unicode-name=\"1F48F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F451\" title=\"crown\" data-aliases=\"\" data-emoji=\"crown\" data-unicode-name=\"1F451\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F622\" title=\"cry\" data-aliases=\"\" data-emoji=\"cry\" data-unicode-name=\"1F622\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63F\" title=\"crying_cat_face\" data-aliases=\"\" data-emoji=\"crying_cat_face\" data-unicode-name=\"1F63F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F498\" title=\"cupid\" data-aliases=\"\" data-emoji=\"cupid\" data-unicode-name=\"1F498\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F483\" title=\"dancer\" data-aliases=\"\" data-emoji=\"dancer\" data-unicode-name=\"1F483\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46F\" title=\"dancers\" data-aliases=\"\" data-emoji=\"dancers\" data-unicode-name=\"1F46F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A8\" title=\"dash\" data-aliases=\"\" data-emoji=\"dash\" data-unicode-name=\"1F4A8\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61E\" title=\"disappointed\" data-aliases=\"\" data-emoji=\"disappointed\" data-unicode-name=\"1F61E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F625\" title=\"disappointed_relieved\" data-aliases=\"\" data-emoji=\"disappointed_relieved\" data-unicode-name=\"1F625\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AB\" title=\"dizzy\" data-aliases=\"\" data-emoji=\"dizzy\" data-unicode-name=\"1F4AB\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F635\" title=\"dizzy_face\" data-aliases=\"\" data-emoji=\"dizzy_face\" data-unicode-name=\"1F635\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F457\" title=\"dress\" data-aliases=\"\" data-emoji=\"dress\" data-unicode-name=\"1F457\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A7\" title=\"droplet\" data-aliases=\"\" data-emoji=\"droplet\" data-unicode-name=\"1F4A7\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F442\" title=\"ear\" data-aliases=\"\" data-emoji=\"ear\" data-unicode-name=\"1F442\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F611\" title=\"expressionless\" data-aliases=\"\" data-emoji=\"expressionless\" data-unicode-name=\"1F611\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F453\" title=\"eyeglasses\" data-aliases=\"\" data-emoji=\"eyeglasses\" data-unicode-name=\"1F453\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F440\" title=\"eyes\" data-aliases=\"\" data-emoji=\"eyes\" data-unicode-name=\"1F440\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46A\" title=\"family\" data-aliases=\"\" data-emoji=\"family\" data-unicode-name=\"1F46A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F628\" title=\"fearful\" data-aliases=\"\" data-emoji=\"fearful\" data-unicode-name=\"1F628\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F525\" title=\"fire\" data-aliases=\":flame:\" data-emoji=\"fire\" data-unicode-name=\"1F525\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270A\" title=\"fist\" data-aliases=\"\" data-emoji=\"fist\" data-unicode-name=\"270A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F633\" title=\"flushed\" data-aliases=\"\" data-emoji=\"flushed\" data-unicode-name=\"1F633\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F463\" title=\"footprints\" data-aliases=\"\" data-emoji=\"footprints\" data-unicode-name=\"1F463\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F626\" title=\"frowning\" data-aliases=\":anguished:\" data-emoji=\"frowning\" data-unicode-name=\"1F626\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48E\" title=\"gem\" data-aliases=\"\" data-emoji=\"gem\" data-unicode-name=\"1F48E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F467\" title=\"girl\" data-aliases=\"\" data-emoji=\"girl\" data-unicode-name=\"1F467\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49A\" title=\"green_heart\" data-aliases=\"\" data-emoji=\"green_heart\" data-unicode-name=\"1F49A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62C\" title=\"grimacing\" data-aliases=\"\" data-emoji=\"grimacing\" data-unicode-name=\"1F62C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F601\" title=\"grin\" data-aliases=\"\" data-emoji=\"grin\" data-unicode-name=\"1F601\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F600\" title=\"grinning\" data-aliases=\"\" data-emoji=\"grinning\" data-unicode-name=\"1F600\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F482\" title=\"guardsman\" data-aliases=\"\" data-emoji=\"guardsman\" data-unicode-name=\"1F482\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F487\" title=\"haircut\" data-aliases=\"\" data-emoji=\"haircut\" data-unicode-name=\"1F487\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45C\" title=\"handbag\" data-aliases=\"\" data-emoji=\"handbag\" data-unicode-name=\"1F45C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F649\" title=\"hear_no_evil\" data-aliases=\"\" data-emoji=\"hear_no_evil\" data-unicode-name=\"1F649\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2764\" title=\"heart\" data-aliases=\"\" data-emoji=\"heart\" data-unicode-name=\"2764\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60D\" title=\"heart_eyes\" data-aliases=\"\" data-emoji=\"heart_eyes\" data-unicode-name=\"1F60D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63B\" title=\"heart_eyes_cat\" data-aliases=\"\" data-emoji=\"heart_eyes_cat\" data-unicode-name=\"1F63B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F493\" title=\"heartbeat\" data-aliases=\"\" data-emoji=\"heartbeat\" data-unicode-name=\"1F493\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F497\" title=\"heartpulse\" data-aliases=\"\" data-emoji=\"heartpulse\" data-unicode-name=\"1F497\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F460\" title=\"high_heel\" data-aliases=\"\" data-emoji=\"high_heel\" data-unicode-name=\"1F460\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62F\" title=\"hushed\" data-aliases=\"\" data-emoji=\"hushed\" data-unicode-name=\"1F62F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47F\" title=\"imp\" data-aliases=\"\" data-emoji=\"imp\" data-unicode-name=\"1F47F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F481\" title=\"information_desk_person\" data-aliases=\"\" data-emoji=\"information_desk_person\" data-unicode-name=\"1F481\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F607\" title=\"innocent\" data-aliases=\"\" data-emoji=\"innocent\" data-unicode-name=\"1F607\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47A\" title=\"japanese_goblin\" data-aliases=\"\" data-emoji=\"japanese_goblin\" data-unicode-name=\"1F47A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F479\" title=\"japanese_ogre\" data-aliases=\"\" data-emoji=\"japanese_ogre\" data-unicode-name=\"1F479\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F456\" title=\"jeans\" data-aliases=\"\" data-emoji=\"jeans\" data-unicode-name=\"1F456\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F602\" title=\"joy\" data-aliases=\"\" data-emoji=\"joy\" data-unicode-name=\"1F602\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F639\" title=\"joy_cat\" data-aliases=\"\" data-emoji=\"joy_cat\" data-unicode-name=\"1F639\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F458\" title=\"kimono\" data-aliases=\"\" data-emoji=\"kimono\" data-unicode-name=\"1F458\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48B\" title=\"kiss\" data-aliases=\"\" data-emoji=\"kiss\" data-unicode-name=\"1F48B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F617\" title=\"kissing\" data-aliases=\"\" data-emoji=\"kissing\" data-unicode-name=\"1F617\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63D\" title=\"kissing_cat\" data-aliases=\"\" data-emoji=\"kissing_cat\" data-unicode-name=\"1F63D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61A\" title=\"kissing_closed_eyes\" data-aliases=\"\" data-emoji=\"kissing_closed_eyes\" data-unicode-name=\"1F61A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F618\" title=\"kissing_heart\" data-aliases=\"\" data-emoji=\"kissing_heart\" data-unicode-name=\"1F618\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F619\" title=\"kissing_smiling_eyes\" data-aliases=\"\" data-emoji=\"kissing_smiling_eyes\" data-unicode-name=\"1F619\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F606\" title=\"laughing\" data-aliases=\":satisfied:\" data-emoji=\"laughing\" data-unicode-name=\"1F606\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F444\" title=\"lips\" data-aliases=\"\" data-emoji=\"lips\" data-unicode-name=\"1F444\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F484\" title=\"lipstick\" data-aliases=\"\" data-emoji=\"lipstick\" data-unicode-name=\"1F484\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48C\" title=\"love_letter\" data-aliases=\"\" data-emoji=\"love_letter\" data-unicode-name=\"1F48C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F468\" title=\"man\" data-aliases=\"\" data-emoji=\"man\" data-unicode-name=\"1F468\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F472\" title=\"man_with_gua_pi_mao\" data-aliases=\"\" data-emoji=\"man_with_gua_pi_mao\" data-unicode-name=\"1F472\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F473\" title=\"man_with_turban\" data-aliases=\"\" data-emoji=\"man_with_turban\" data-unicode-name=\"1F473\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45E\" title=\"mans_shoe\" data-aliases=\"\" data-emoji=\"mans_shoe\" data-unicode-name=\"1F45E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F637\" title=\"mask\" data-aliases=\"\" data-emoji=\"mask\" data-unicode-name=\"1F637\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F486\" title=\"massage\" data-aliases=\"\" data-emoji=\"massage\" data-unicode-name=\"1F486\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AA\" title=\"muscle\" data-aliases=\"\" data-emoji=\"muscle\" data-unicode-name=\"1F4AA\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F485\" title=\"nail_care\" data-aliases=\"\" data-emoji=\"nail_care\" data-unicode-name=\"1F485\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F454\" title=\"necktie\" data-aliases=\"\" data-emoji=\"necktie\" data-unicode-name=\"1F454\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F610\" title=\"neutral_face\" data-aliases=\"\" data-emoji=\"neutral_face\" data-unicode-name=\"1F610\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F645\" title=\"no_good\" data-aliases=\"\" data-emoji=\"no_good\" data-unicode-name=\"1F645\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F636\" title=\"no_mouth\" data-aliases=\"\" data-emoji=\"no_mouth\" data-unicode-name=\"1F636\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F443\" title=\"nose\" data-aliases=\"\" data-emoji=\"nose\" data-unicode-name=\"1F443\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44C\" title=\"ok_hand\" data-aliases=\"\" data-emoji=\"ok_hand\" data-unicode-name=\"1F44C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F646\" title=\"ok_woman\" data-aliases=\"\" data-emoji=\"ok_woman\" data-unicode-name=\"1F646\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F474\" title=\"older_man\" data-aliases=\"\" data-emoji=\"older_man\" data-unicode-name=\"1F474\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F475\" title=\"older_woman\" data-aliases=\":grandma:\" data-emoji=\"older_woman\" data-unicode-name=\"1F475\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F450\" title=\"open_hands\" data-aliases=\"\" data-emoji=\"open_hands\" data-unicode-name=\"1F450\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62E\" title=\"open_mouth\" data-aliases=\"\" data-emoji=\"open_mouth\" data-unicode-name=\"1F62E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F614\" title=\"pensive\" data-aliases=\"\" data-emoji=\"pensive\" data-unicode-name=\"1F614\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F623\" title=\"persevere\" data-aliases=\"\" data-emoji=\"persevere\" data-unicode-name=\"1F623\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64D\" title=\"person_frowning\" data-aliases=\"\" data-emoji=\"person_frowning\" data-unicode-name=\"1F64D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F471\" title=\"person_with_blond_hair\" data-aliases=\"\" data-emoji=\"person_with_blond_hair\" data-unicode-name=\"1F471\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64E\" title=\"person_with_pouting_face\" data-aliases=\"\" data-emoji=\"person_with_pouting_face\" data-unicode-name=\"1F64E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F447\" title=\"point_down\" data-aliases=\"\" data-emoji=\"point_down\" data-unicode-name=\"1F447\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F448\" title=\"point_left\" data-aliases=\"\" data-emoji=\"point_left\" data-unicode-name=\"1F448\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F449\" title=\"point_right\" data-aliases=\"\" data-emoji=\"point_right\" data-unicode-name=\"1F449\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-261D\" title=\"point_up\" data-aliases=\"\" data-emoji=\"point_up\" data-unicode-name=\"261D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F446\" title=\"point_up_2\" data-aliases=\"\" data-emoji=\"point_up_2\" data-unicode-name=\"1F446\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A9\" title=\"poop\" data-aliases=\":shit: :hankey: :poo:\" data-emoji=\"poop\" data-unicode-name=\"1F4A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45D\" title=\"pouch\" data-aliases=\"\" data-emoji=\"pouch\" data-unicode-name=\"1F45D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63E\" title=\"pouting_cat\" data-aliases=\"\" data-emoji=\"pouting_cat\" data-unicode-name=\"1F63E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64F\" title=\"pray\" data-aliases=\"\" data-emoji=\"pray\" data-unicode-name=\"1F64F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F478\" title=\"princess\" data-aliases=\"\" data-emoji=\"princess\" data-unicode-name=\"1F478\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44A\" title=\"punch\" data-aliases=\"\" data-emoji=\"punch\" data-unicode-name=\"1F44A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49C\" title=\"purple_heart\" data-aliases=\"\" data-emoji=\"purple_heart\" data-unicode-name=\"1F49C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45B\" title=\"purse\" data-aliases=\"\" data-emoji=\"purse\" data-unicode-name=\"1F45B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F621\" title=\"rage\" data-aliases=\"\" data-emoji=\"rage\" data-unicode-name=\"1F621\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270B\" title=\"raised_hand\" data-aliases=\"\" data-emoji=\"raised_hand\" data-unicode-name=\"270B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64C\" title=\"raised_hands\" data-aliases=\"\" data-emoji=\"raised_hands\" data-unicode-name=\"1F64C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64B\" title=\"raising_hand\" data-aliases=\"\" data-emoji=\"raising_hand\" data-unicode-name=\"1F64B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-263A\" title=\"relaxed\" data-aliases=\"\" data-emoji=\"relaxed\" data-unicode-name=\"263A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60C\" title=\"relieved\" data-aliases=\"\" data-emoji=\"relieved\" data-unicode-name=\"1F60C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49E\" title=\"revolving_hearts\" data-aliases=\"\" data-emoji=\"revolving_hearts\" data-unicode-name=\"1F49E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F380\" title=\"ribbon\" data-aliases=\"\" data-emoji=\"ribbon\" data-unicode-name=\"1F380\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48D\" title=\"ring\" data-aliases=\"\" data-emoji=\"ring\" data-unicode-name=\"1F48D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3C3\" title=\"runner\" data-aliases=\"\" data-emoji=\"runner\" data-unicode-name=\"1F3C3\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3BD\" title=\"running_shirt_with_sash\" data-aliases=\"\" data-emoji=\"running_shirt_with_sash\" data-unicode-name=\"1F3BD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F461\" title=\"sandal\" data-aliases=\"\" data-emoji=\"sandal\" data-unicode-name=\"1F461\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F631\" title=\"scream\" data-aliases=\"\" data-emoji=\"scream\" data-unicode-name=\"1F631\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F640\" title=\"scream_cat\" data-aliases=\"\" data-emoji=\"scream_cat\" data-unicode-name=\"1F640\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F648\" title=\"see_no_evil\" data-aliases=\"\" data-emoji=\"see_no_evil\" data-unicode-name=\"1F648\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F455\" title=\"shirt\" data-aliases=\"\" data-emoji=\"shirt\" data-unicode-name=\"1F455\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F480\" title=\"skull\" data-aliases=\":skeleton:\" data-emoji=\"skull\" data-unicode-name=\"1F480\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F634\" title=\"sleeping\" data-aliases=\"\" data-emoji=\"sleeping\" data-unicode-name=\"1F634\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62A\" title=\"sleepy\" data-aliases=\"\" data-emoji=\"sleepy\" data-unicode-name=\"1F62A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F604\" title=\"smile\" data-aliases=\"\" data-emoji=\"smile\" data-unicode-name=\"1F604\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F638\" title=\"smile_cat\" data-aliases=\"\" data-emoji=\"smile_cat\" data-unicode-name=\"1F638\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F603\" title=\"smiley\" data-aliases=\"\" data-emoji=\"smiley\" data-unicode-name=\"1F603\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63A\" title=\"smiley_cat\" data-aliases=\"\" data-emoji=\"smiley_cat\" data-unicode-name=\"1F63A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F608\" title=\"smiling_imp\" data-aliases=\"\" data-emoji=\"smiling_imp\" data-unicode-name=\"1F608\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60F\" title=\"smirk\" data-aliases=\"\" data-emoji=\"smirk\" data-unicode-name=\"1F60F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63C\" title=\"smirk_cat\" data-aliases=\"\" data-emoji=\"smirk_cat\" data-unicode-name=\"1F63C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62D\" title=\"sob\" data-aliases=\"\" data-emoji=\"sob\" data-unicode-name=\"1F62D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2728\" title=\"sparkles\" data-aliases=\"\" data-emoji=\"sparkles\" data-unicode-name=\"2728\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F496\" title=\"sparkling_heart\" data-aliases=\"\" data-emoji=\"sparkling_heart\" data-unicode-name=\"1F496\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64A\" title=\"speak_no_evil\" data-aliases=\"\" data-emoji=\"speak_no_evil\" data-unicode-name=\"1F64A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AC\" title=\"speech_balloon\" data-aliases=\"\" data-emoji=\"speech_balloon\" data-unicode-name=\"1F4AC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F31F\" title=\"star2\" data-aliases=\"\" data-emoji=\"star2\" data-unicode-name=\"1F31F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61B\" title=\"stuck_out_tongue\" data-aliases=\"\" data-emoji=\"stuck_out_tongue\" data-unicode-name=\"1F61B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61D\" title=\"stuck_out_tongue_closed_eyes\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_closed_eyes\" data-unicode-name=\"1F61D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61C\" title=\"stuck_out_tongue_winking_eye\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_winking_eye\" data-unicode-name=\"1F61C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60E\" title=\"sunglasses\" data-aliases=\"\" data-emoji=\"sunglasses\" data-unicode-name=\"1F60E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F613\" title=\"sweat\" data-aliases=\"\" data-emoji=\"sweat\" data-unicode-name=\"1F613\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A6\" title=\"sweat_drops\" data-aliases=\"\" data-emoji=\"sweat_drops\" data-unicode-name=\"1F4A6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F605\" title=\"sweat_smile\" data-aliases=\"\" data-emoji=\"sweat_smile\" data-unicode-name=\"1F605\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AD\" title=\"thought_balloon\" data-aliases=\"\" data-emoji=\"thought_balloon\" data-unicode-name=\"1F4AD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44E\" title=\"thumbsdown\" data-aliases=\":-1:\" data-emoji=\"thumbsdown\" data-unicode-name=\"1F44E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44D\" title=\"thumbsup\" data-aliases=\":+1:\" data-emoji=\"thumbsup\" data-unicode-name=\"1F44D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62B\" title=\"tired_face\" data-aliases=\"\" data-emoji=\"tired_face\" data-unicode-name=\"1F62B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F445\" title=\"tongue\" data-aliases=\"\" data-emoji=\"tongue\" data-unicode-name=\"1F445\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3A9\" title=\"tophat\" data-aliases=\"\" data-emoji=\"tophat\" data-unicode-name=\"1F3A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F624\" title=\"triumph\" data-aliases=\"\" data-emoji=\"triumph\" data-unicode-name=\"1F624\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F495\" title=\"two_hearts\" data-aliases=\"\" data-emoji=\"two_hearts\" data-unicode-name=\"1F495\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46C\" title=\"two_men_holding_hands\" data-aliases=\"\" data-emoji=\"two_men_holding_hands\" data-unicode-name=\"1F46C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46D\" title=\"two_women_holding_hands\" data-aliases=\"\" data-emoji=\"two_women_holding_hands\" data-unicode-name=\"1F46D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F612\" title=\"unamused\" data-aliases=\"\" data-emoji=\"unamused\" data-unicode-name=\"1F612\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270C\" title=\"v\" data-aliases=\"\" data-emoji=\"v\" data-unicode-name=\"270C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F6B6\" title=\"walking\" data-aliases=\"\" data-emoji=\"walking\" data-unicode-name=\"1F6B6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44B\" title=\"wave\" data-aliases=\"\" data-emoji=\"wave\" data-unicode-name=\"1F44B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F629\" title=\"weary\" data-aliases=\"\" data-emoji=\"weary\" data-unicode-name=\"1F629\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F609\" title=\"wink\" data-aliases=\"\" data-emoji=\"wink\" data-unicode-name=\"1F609\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F469\" title=\"woman\" data-aliases=\"\" data-emoji=\"woman\" data-unicode-name=\"1F469\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45A\" title=\"womans_clothes\" data-aliases=\"\" data-emoji=\"womans_clothes\" data-unicode-name=\"1F45A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F452\" title=\"womans_hat\" data-aliases=\"\" data-emoji=\"womans_hat\" data-unicode-name=\"1F452\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61F\" title=\"worried\" data-aliases=\"\" data-emoji=\"worried\" data-unicode-name=\"1F61F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49B\" title=\"yellow_heart\" data-aliases=\"\" data-emoji=\"yellow_heart\" data-unicode-name=\"1F49B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60B\" title=\"yum\" data-aliases=\"\" data-emoji=\"yum\" data-unicode-name=\"1F60B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A4\" title=\"zzz\" data-aliases=\"\" data-emoji=\"zzz\" data-unicode-name=\"1F4A4\"></div>\n </button>\n </li>\n </ul>\n </div>\n</div>"; -}).call(this); +}).call(window); diff --git a/spec/javascripts/fixtures/environments/environments_folder_view.html.haml b/spec/javascripts/fixtures/environments/environments_folder_view.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..aceec1397303197c20bc047cb1267a05c584071d --- /dev/null +++ b/spec/javascripts/fixtures/environments/environments_folder_view.html.haml @@ -0,0 +1,7 @@ +%div + #environments-folder-list-view{ data: { "can-create-deployment" => "true", + "can-read-environment" => "true", + "css-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/spec/javascripts/fixtures/project_title.html.haml b/spec/javascripts/fixtures/project_title.html.haml deleted file mode 100644 index 9d1f78771167c6f40febaa9a1fd2005a2450e334..0000000000000000000000000000000000000000 --- a/spec/javascripts/fixtures/project_title.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -.header-content - %h1.title - %a - GitLab Org - %a.project-item-select-holder{href: "/gitlab-org/gitlab-test"} - GitLab Test - %i.fa.chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle{ "data-toggle" => "dropdown", "data-target" => ".header-content", "data-order-by" => "last_activity_at" } - .js-dropdown-menu-projects - .dropdown-menu.dropdown-select.dropdown-menu-projects - .dropdown-title - %span Go to a project - %button.dropdown-title-button.dropdown-menu-close{"aria-label" => "Close", type: "button"} - %i.fa.fa-times.dropdown-menu-close-icon - .dropdown-input - %input.dropdown-input-field{id: "", placeholder: "Search your projects", type: "search", value: ""} - %i.fa.fa-search.dropdown-input-search - %i.fa.fa-times.dropdown-input-clear.js-dropdown-input-clear{role: "button"} - .dropdown-content - .dropdown-loading - %i.fa.fa-spinner.fa-spin diff --git a/spec/javascripts/gfm_auto_complete_spec.js.es6 b/spec/javascripts/gfm_auto_complete_spec.js.es6 index c61c32f8a1331332492b5bba1c947cba5eb0f581..5dfa4008fbd5596b6ad1c961070238ba1a62cd4e 100644 --- a/spec/javascripts/gfm_auto_complete_spec.js.es6 +++ b/spec/javascripts/gfm_auto_complete_spec.js.es6 @@ -1,3 +1,5 @@ +/* eslint no-param-reassign: "off" */ + require('~/gfm_auto_complete'); require('vendor/jquery.caret'); require('vendor/jquery.atwho'); @@ -63,6 +65,61 @@ describe('GfmAutoComplete', function () { }); }); + describe('DefaultOptions.matcher', function () { + const defaultMatcher = (context, flag, subtext) => ( + GfmAutoComplete.DefaultOptions.matcher.call(context, flag, subtext) + ); + + const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%']; + const otherFlags = ['/', ':']; + const flags = flagsUseDefaultMatcher.concat(otherFlags); + + const flagsHash = flags.reduce((hash, el) => { hash[el] = null; return hash; }, {}); + const atwhoInstance = { setting: {}, app: { controllers: flagsHash } }; + + const minLen = 1; + const maxLen = 20; + const argumentSize = [minLen, maxLen / 2, maxLen]; + + const allowedSymbols = ['', 'a', 'n', 'z', 'A', 'Z', 'N', '0', '5', '9', 'А', 'а', 'Я', 'я', '.', '\'', '+', '-', '_']; + const jointAllowedSymbols = allowedSymbols.join(''); + + describe('should match regular symbols', () => { + flagsUseDefaultMatcher.forEach((flag) => { + allowedSymbols.forEach((symbol) => { + argumentSize.forEach((size) => { + const query = new Array(size + 1).join(symbol); + const subtext = flag + query; + + it(`matches argument "${flag}" with query "${subtext}"`, () => { + expect(defaultMatcher(atwhoInstance, flag, subtext)).toBe(query); + }); + }); + }); + + it(`matches combination of allowed symbols for flag "${flag}"`, () => { + const subtext = flag + jointAllowedSymbols; + + expect(defaultMatcher(atwhoInstance, flag, subtext)).toBe(jointAllowedSymbols); + }); + }); + }); + + describe('should not match special sequences', () => { + const ShouldNotBeFollowedBy = flags.concat(['\x00', '\x10', '\x3f', '\n', ' ']); + + flagsUseDefaultMatcher.forEach((atSign) => { + ShouldNotBeFollowedBy.forEach((followedSymbol) => { + const seq = atSign + followedSymbol; + + it(`should not match "${seq}"`, () => { + expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null); + }); + }); + }); + }); + }); + describe('isLoading', function () { it('should be true with loading data object item', function () { expect(GfmAutoComplete.isLoading({ name: 'loading' })).toBe(true); diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js index 2b263b71b7d5d2979882e38b5c3110aabd128ddb..576b6cfefdcc92f5aa24e2506c617749d71fcdb2 100644 --- a/spec/javascripts/header_spec.js +++ b/spec/javascripts/header_spec.js @@ -50,4 +50,4 @@ require('~/lib/utils/text_utility'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 5b0b7aa7903d633d4c967766e66023553c469a6d..e7530f6138593e2e9c84c3142bd3984782c15587 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -105,6 +105,7 @@ require('~/issue'); expectIssueState(false); expect($btnClose).toHaveProp('disabled', false); + expect($('.issue_counter')).toHaveText(0); }); it('fails to close an issue with success:false', function() { @@ -121,6 +122,7 @@ require('~/issue'); expectIssueState(true); expect($btnClose).toHaveProp('disabled', false); expectErrorMessage(); + expect($('.issue_counter')).toHaveText(1); }); it('fails to closes an issue with HTTP error', function() { @@ -135,6 +137,7 @@ require('~/issue'); expectIssueState(true); expect($btnClose).toHaveProp('disabled', true); expectErrorMessage(); + expect($('.issue_counter')).toHaveText(1); }); }); @@ -159,6 +162,7 @@ require('~/issue'); expectIssueState(true); expect($btnReopen).toHaveProp('disabled', false); + expect($('.issue_counter')).toHaveText(1); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index 006ede21093d313a66823351149b3dec0590520c..f4d3e77e515aa8f78f99c5ab1aa7f75392d6b4a9 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -108,6 +108,30 @@ require('~/lib/utils/common_utils'); }); }); + describe('gl.utils.parseIntPagination', () => { + it('should parse to integers all string values and return pagination object', () => { + const pagination = { + 'X-PER-PAGE': 10, + 'X-PAGE': 2, + 'X-TOTAL': 30, + 'X-TOTAL-PAGES': 3, + 'X-NEXT-PAGE': 3, + 'X-PREV-PAGE': 1, + }; + + const expectedPagination = { + perPage: 10, + page: 2, + total: 30, + totalPages: 3, + nextPage: 3, + previousPage: 1, + }; + + expect(gl.utils.parseIntPagination(pagination)).toEqual(expectedPagination); + }); + }); + describe('gl.utils.isMetaClick', () => { it('should identify meta click on Windows/Linux', () => { const e = { diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index 8b196f7720f7aa0a7e8ddeff6f3eac88c84e4985..a0b2ebc221bcaf20736875099c8769b548b63906 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -227,4 +227,4 @@ require('~/line_highlighter'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 25cfa9e947971bd55946defbee41833503cb940e..fd97dced8705431f2ca3aafce91a1761956f4b63 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -6,9 +6,9 @@ require('~/merge_request'); (function() { describe('MergeRequest', function() { return describe('task lists', function() { - preloadFixtures('static/merge_requests_show.html.raw'); + preloadFixtures('merge_requests/merge_request_with_task_list.html.raw'); beforeEach(function() { - loadFixtures('static/merge_requests_show.html.raw'); + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); return this.merge = new MergeRequest(); }); it('modifies the Markdown field', function() { @@ -19,11 +19,11 @@ require('~/merge_request'); return it('submits an ajax request on tasklist:changed', function() { spyOn(jQuery, 'ajax').and.callFake(function(req) { expect(req.type).toBe('PATCH'); - expect(req.url).toBe('/foo'); + expect(req.url).toBe(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`); return expect(req.data.merge_request.description).not.toBe(null); }); return $('.js-task-list-field').trigger('tasklist:changed'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 5b0c124962c06ac39f5d4ebd8f6c764b78e36c63..7506e6ab49e5580e79f5aa157853b14fac024681 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -209,4 +209,4 @@ require('vendor/jquery.scrollTo'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index 8cefdd2409d4f2239e9513da2fe1ae7441be1b21..d5193b41c332afc1ee5e8fd7732d61d1ee01b1b1 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -189,4 +189,4 @@ require('~/lib/utils/datetime_utility'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index 1d014502c2aebde4b66eac9649789e2cbbf92281..f132537b943aa597cb1f4815969eee5d94c729dc 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -166,4 +166,4 @@ require('~/new_branch_form'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index af495787c54042865069c693e58711e42a429088..d81a5bbb6a518adf6a89f9b002f3420a1f2937bb 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -35,15 +35,13 @@ require('~/lib/utils/text_utility'); expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); - it('submits the form on tasklist:changed', function() { - var submitted = false; - $('form').on('submit', function(e) { - submitted = true; - e.preventDefault(); + it('submits an ajax request on tasklist:changed', function() { + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PATCH'); + expect(req.url).toBe('http://test.host/frontend-fixtures/issues-project/notes/1'); + return expect(req.data.note).not.toBe(null); }); - $('.js-task-list-field').trigger('tasklist:changed'); - expect(submitted).toBe(true); }); }); @@ -75,4 +73,4 @@ require('~/lib/utils/text_utility'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/project_dashboard_spec.js.es6 b/spec/javascripts/project_dashboard_spec.js.es6 deleted file mode 100644 index 24833b4eb57760eee1225d2e502c1d20d39529bb..0000000000000000000000000000000000000000 --- a/spec/javascripts/project_dashboard_spec.js.es6 +++ /dev/null @@ -1,86 +0,0 @@ -require('~/sidebar'); - -(() => { - describe('Project dashboard page', () => { - let $pageWithSidebar = null; - let $sidebarToggle = null; - let sidebar = null; - const fixtureTemplate = 'projects/dashboard.html.raw'; - - const assertSidebarStateExpanded = (shouldBeExpanded) => { - expect(sidebar.isExpanded).toBe(shouldBeExpanded); - expect($pageWithSidebar.hasClass('page-sidebar-expanded')).toBe(shouldBeExpanded); - }; - - preloadFixtures(fixtureTemplate); - beforeEach(() => { - loadFixtures(fixtureTemplate); - - $pageWithSidebar = $('.page-with-sidebar'); - $sidebarToggle = $('.toggle-nav-collapse'); - - // otherwise instantiating the Sidebar for the second time - // won't do anything, as the Sidebar is a singleton class - gl.Sidebar.singleton = null; - sidebar = new gl.Sidebar(); - }); - - it('can show the sidebar when the toggler is clicked', () => { - assertSidebarStateExpanded(false); - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - }); - - it('should dismiss the sidebar when clone button clicked', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - const cloneButton = $('.project-clone-holder a.clone-dropdown-btn'); - cloneButton.click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when download button clicked', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - const downloadButton = $('.project-action-button .btn:has(i.fa-download)'); - downloadButton.click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when add button clicked', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - const addButton = $('.project-action-button .btn:has(i.fa-plus)'); - addButton.click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when notification button clicked', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - const notifButton = $('.js-notification-toggle-btns .notifications-btn'); - notifButton.click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when clicking on the body', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - $('body').click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when clicking on the project description header', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - $('.project-home-panel').click(); - assertSidebarStateExpanded(false); - }); - }); -})(); diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index bfe3d2df79d64f398fb998e387abb9e7ca940f6d..69d9587771f302144ecc5d5f35a7f7f6d2bce4f5 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -10,11 +10,11 @@ require('~/project'); (function() { describe('Project Title', function() { - preloadFixtures('static/project_title.html.raw'); + preloadFixtures('issues/open-issue.html.raw'); loadJSONFixtures('projects.json'); beforeEach(function() { - loadFixtures('static/project_title.html.raw'); + loadFixtures('issues/open-issue.html.raw'); window.gon = {}; window.gon.api_version = 'v3'; @@ -38,15 +38,12 @@ require('~/project'); return spyOn(jQuery, 'ajax').and.callFake(fakeAjaxResponse.bind(_this)); }; })(this)); - it('to show on toggle click', (function(_this) { - return function() { - $('.js-projects-dropdown-toggle').click(); - return expect($('.header-content').hasClass('open')).toBe(true); - }; - })(this)); - return it('hide dropdown', function() { - $(".dropdown-menu-close-icon").click(); - return expect($('.header-content').hasClass('open')).toBe(false); + it('toggles dropdown', function() { + var menu = $('.js-dropdown-menu-projects'); + $('.js-projects-dropdown-toggle').click(); + expect(menu).toHaveClass('open'); + menu.find('.dropdown-menu-close-icon').click(); + expect(menu).not.toHaveClass('open'); }); }); @@ -54,4 +51,4 @@ require('~/project'); window.gon = {}; }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 9284af8a8d91bf2c666367f51eb6cf68f5a3b2b7..4ac7e911740bc69ccf4e5ad32fb20999b722f2af 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -79,4 +79,4 @@ require('~/extensions/jquery.js'); expect(todoToggleSpy.calls.count()).toEqual(1); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 9572b52ec1ed1f746bfa461817f4db3b86beb51e..cb8754581c094630908e2a3593c26ab9d2e939a1 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -175,4 +175,4 @@ require('vendor/fuzzaldrin-plus'); expect(enterKeyEvent.isDefaultPrevented()).toBe(true); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index 602ac01aec3c50d9a16865d13b9d9b8a52e98e23..ffff643e37151d46e1097ef13a45c72c89a27abd 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -79,4 +79,4 @@ require('~/shortcuts_issuable'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js index c0c3837d1f40b09e6c36bde7b5e270ab73a82ba5..cea223bd243c337adbadc6a0083c4b63567e190d 100644 --- a/spec/javascripts/syntax_highlight_spec.js +++ b/spec/javascripts/syntax_highlight_spec.js @@ -41,4 +41,4 @@ require('~/syntax_highlight'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index cba1af4daa433774c286bd5ed206abc01496637b..af2d02b6b29908ec403f992e930dcaea2582c12d 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -69,4 +69,4 @@ require('./mock_u2f_device'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js index 287bfb4138b6d6174d1a2f9a6c79cc3fa74a550f..6677fe9c1ee065ce9ce7533886bb1e279e84f32a 100644 --- a/spec/javascripts/u2f/mock_u2f_device.js +++ b/spec/javascripts/u2f/mock_u2f_device.js @@ -30,4 +30,4 @@ return MockU2FDevice; })(); -}).call(this); +}).call(window); diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 10578c2c4b56004926a09f578169366f657aef44..0f390c8b98056343e81ecb3d51e656b3bb04f741 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -74,4 +74,4 @@ require('./mock_u2f_device'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 index e84f0dcfe6741aa1608069d691708b25e4c7e903..dd495cb43bc855a71dabc1be0622fdaa7d53f811 100644 --- a/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 @@ -34,7 +34,7 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: '1' } }); expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); + expect(changeChanges.two).toEqual(null); }); it('should go to the previous page', () => { @@ -55,7 +55,7 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: 'Prev' } }); expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); + expect(changeChanges.two).toEqual(null); }); it('should go to the next page', () => { @@ -76,7 +76,7 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: 'Next' } }); expect(changeChanges.one).toEqual(5); - expect(changeChanges.two).toEqual('all'); + expect(changeChanges.two).toEqual(null); }); it('should go to the last page', () => { @@ -97,7 +97,7 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: 'Last >>' } }); expect(changeChanges.one).toEqual(10); - expect(changeChanges.two).toEqual('all'); + expect(changeChanges.two).toEqual(null); }); it('should go to the first page', () => { @@ -118,7 +118,7 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: '<< First' } }); expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); + expect(changeChanges.two).toEqual(null); }); it('should do nothing', () => { @@ -139,7 +139,7 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: '...' } }); expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); + expect(changeChanges.two).toEqual(null); }); }); diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index ce33a6814aa887ca82c375803c8b6912750a6815..99515f2e5f252b6d7c6d37db98003d12d91ba348 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -76,4 +76,4 @@ require('~/zen_mode'); keyCode: 27 })); }; -}).call(this); +}).call(window); diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 456dbac0698ffa2e61ef979654f0ffc2b43b4235..11607d4fb26916da7327dfc11048b91739adbcdf 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -311,7 +311,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end end - describe '#issues_per_Project' do + describe '#issues_per_project' do context 'using an internal issue tracker' do it 'returns a Hash containing the issues per project' do doc = Nokogiri::HTML.fragment('') @@ -346,4 +346,26 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end end end + + describe '.references_in' do + let(:merge_request) { create(:merge_request) } + + it 'yields valid references' do + expect do |b| + described_class.references_in(issue.to_reference, &b) + end.to yield_with_args(issue.to_reference, issue.iid, nil, nil, MatchData) + end + + it "doesn't yield invalid references" do + expect do |b| + described_class.references_in('#0', &b) + end.not_to yield_control + end + + it "doesn't yield unsupported references" do + expect do |b| + described_class.references_in(merge_request.to_reference, &b) + end.not_to yield_control + end + end end diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb index 6c71e98066bf0b90487fa8a8144269e6594d63d9..91c43f2bdc0cb82350fb4c78892367e1cb77f059 100644 --- a/spec/lib/gitlab/data_builder/build_spec.rb +++ b/spec/lib/gitlab/data_builder/build_spec.rb @@ -17,5 +17,31 @@ describe Gitlab::DataBuilder::Build do it { expect(data[:build_allow_failure]).to eq(false) } it { expect(data[:project_id]).to eq(build.project.id) } it { expect(data[:project_name]).to eq(build.project.name_with_namespace) } + + context 'commit author_url' do + context 'when no commit present' do + let(:build) { create(:ci_build) } + + it 'sets to mailing address of git_author_email' do + expect(data[:commit][:author_url]).to eq("mailto:#{build.pipeline.git_author_email}") + end + end + + context 'when commit present but has no author' do + let(:build) { create(:ci_build, :with_commit) } + + it 'sets to mailing address of git_author_email' do + expect(data[:commit][:author_url]).to eq("mailto:#{build.pipeline.git_author_email}") + end + end + + context 'when commit and author are present' do + let(:build) { create(:ci_build, :with_commit_and_author) } + + it 'sets to GitLab user url' do + expect(data[:commit][:author_url]).to eq(Gitlab::Routing.url_helpers.user_url(username: build.commit.author.username)) + end + end + end end end diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json new file mode 100644 index 0000000000000000000000000000000000000000..a78836c3c3456314c53fbc595c5a9a01184d6061 --- /dev/null +++ b/spec/lib/gitlab/import_export/project.light.json @@ -0,0 +1,48 @@ +{ + "description": "Nisi et repellendus ut enim quo accusamus vel magnam.", + "visibility_level": 10, + "archived": false, + "labels": [ + { + "id": 2, + "title": "test2", + "color": "#428bca", + "project_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "type": "ProjectLabel", + "priorities": [ + ] + }, + { + "id": 3, + "title": "test3", + "color": "#428bca", + "group_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "project_id": null, + "type": "GroupLabel", + "priorities": [ + { + "id": 1, + "project_id": 5, + "label_id": 1, + "priority": 1, + "created_at": "2016-10-18T09:35:43.338Z", + "updated_at": "2016-10-18T09:35:43.338Z" + } + ] + } + ], + "snippets": [ + + ], + "hooks": [ + + ] +} \ No newline at end of file diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 0af13ba8e47319fc65577d99c7be4f750a07f8f5..f4a21c24fa1c1ecb245e5629baaf0311004e0ebd 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -3,24 +3,24 @@ include ImportExport::CommonUtil describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do describe 'restore project tree' do - let(:user) { create(:user) } - let(:namespace) { create(:namespace, owner: user) } - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } - let!(:project) { create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } - let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } - let(:restored_project_json) { project_tree_restorer.restore } + before(:context) do + @user = create(:user) - before do - allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') + RSpec::Mocks.with_temporary_scope do + @shared = Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') + allow(@shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') + @project = create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') + project_tree_restorer = described_class.new(user: @user, shared: @shared, project: @project) + @restored_project_json = project_tree_restorer.restore + end end context 'JSON' do it 'restores models based on JSON' do - expect(restored_project_json).to be true + expect(@restored_project_json).to be true end it 'restore correct project features' do - restored_project_json project = Project.find_by_path('project') expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED) @@ -31,62 +31,42 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do end it 'has the same label associated to two issues' do - restored_project_json - expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2) end it 'has milestones associated to two separate issues' do - restored_project_json - expect(Milestone.find_by_description('test milestone').issues.count).to eq(2) end it 'creates a valid pipeline note' do - restored_project_json - expect(Ci::Pipeline.first.notes).not_to be_empty end it 'restores pipelines with missing ref' do - restored_project_json - expect(Ci::Pipeline.where(ref: nil)).not_to be_empty end it 'restores the correct event with symbolised data' do - restored_project_json - expect(Event.where.not(data: nil).first.data[:ref]).not_to be_empty end it 'preserves updated_at on issues' do - restored_project_json - issue = Issue.where(description: 'Aliquam enim illo et possimus.').first expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC') end it 'contains the merge access levels on a protected branch' do - restored_project_json - expect(ProtectedBranch.first.merge_access_levels).not_to be_empty end it 'contains the push access levels on a protected branch' do - restored_project_json - expect(ProtectedBranch.first.push_access_levels).not_to be_empty end context 'event at forth level of the tree' do let(:event) { Event.where(title: 'test levels').first } - before do - restored_project_json - end - it 'restores the event' do expect(event).not_to be_nil end @@ -99,77 +79,40 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do it 'has the correct data for merge request st_diffs' do # makes sure we are renaming the custom method +utf8_st_diffs+ into +st_diffs+ - expect { restored_project_json }.to change(MergeRequestDiff.where.not(st_diffs: nil), :count).by(9) + expect(MergeRequestDiff.where.not(st_diffs: nil).count).to eq(9) end it 'has labels associated to label links, associated to issues' do - restored_project_json - expect(Label.first.label_links.first.target).not_to be_nil end it 'has project labels' do - restored_project_json - expect(ProjectLabel.count).to eq(2) end it 'has no group labels' do - restored_project_json - expect(GroupLabel.count).to eq(0) end - context 'with group' do - let!(:project) do - create(:empty_project, - :builds_disabled, - :issues_disabled, - name: 'project', - path: 'project', - group: create(:group)) - end - - it 'has group labels' do - restored_project_json - - expect(GroupLabel.count).to eq(1) - end - - it 'has label priorities' do - restored_project_json - - expect(GroupLabel.first.priorities).not_to be_empty - end - end - it 'has a project feature' do - restored_project_json - - expect(project.project_feature).not_to be_nil + expect(@project.project_feature).not_to be_nil end it 'restores the correct service' do - restored_project_json - expect(CustomIssueTrackerService.first).not_to be_nil end context 'Merge requests' do - before do - restored_project_json - end - it 'always has the new project as a target' do - expect(MergeRequest.find_by_title('MR1').target_project).to eq(project) + expect(MergeRequest.find_by_title('MR1').target_project).to eq(@project) end it 'has the same source project as originally if source/target are the same' do - expect(MergeRequest.find_by_title('MR1').source_project).to eq(project) + expect(MergeRequest.find_by_title('MR1').source_project).to eq(@project) end it 'has the new project as target if source/target differ' do - expect(MergeRequest.find_by_title('MR2').target_project).to eq(project) + expect(MergeRequest.find_by_title('MR2').target_project).to eq(@project) end it 'has no source if source/target differ' do @@ -177,39 +120,71 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do end end - context 'project.json file access check' do - it 'does not read a symlink' do - Dir.mktmpdir do |tmpdir| - setup_symlink(tmpdir, 'project.json') - allow(shared).to receive(:export_path).and_call_original - - restored_project_json + context 'tokens are regenerated' do + it 'has a new CI trigger token' do + expect(Ci::Trigger.where(token: 'cdbfasdf44a5958c83654733449e585')).to be_empty + end - expect(shared.errors.first).not_to include('test') - end + it 'has a new CI build token' do + expect(Ci::Build.where(token: 'abcd')).to be_empty end end + end + end - context 'when there is an existing build with build token' do - it 'restores project json correctly' do - create(:ci_build, token: 'abcd') + context 'Light JSON' do + let(:user) { create(:user) } + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } + let!(:project) { create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } + let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } + let(:restored_project_json) { project_tree_restorer.restore } - expect(restored_project_json).to be true - end - end + before do + allow(ImportExport).to receive(:project_filename).and_return('project.light.json') + allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') + end + + context 'project.json file access check' do + it 'does not read a symlink' do + Dir.mktmpdir do |tmpdir| + setup_symlink(tmpdir, 'project.json') + allow(shared).to receive(:export_path).and_call_original - context 'tokens are regenerated' do - before do restored_project_json - end - it 'has a new CI trigger token' do - expect(Ci::Trigger.where(token: 'cdbfasdf44a5958c83654733449e585')).to be_empty + expect(shared.errors.first).not_to include('test') end + end + end - it 'has a new CI build token' do - expect(Ci::Build.where(token: 'abcd')).to be_empty - end + context 'when there is an existing build with build token' do + it 'restores project json correctly' do + create(:ci_build, token: 'abcd') + + expect(restored_project_json).to be true + end + end + + context 'with group' do + let!(:project) do + create(:empty_project, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: create(:group)) + end + + before do + restored_project_json + end + + it 'has group labels' do + expect(GroupLabel.count).to eq(1) + end + + it 'has label priorities' do + expect(GroupLabel.first.priorities).not_to be_empty end end end diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..168a59e51391ae7e18e55989f121f87ff5cc61ca --- /dev/null +++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::RepoRestorer, services: true do + describe 'bundle a project Git repo' do + let(:user) { create(:user) } + let!(:project_with_repo) { create(:project, :test_repo, name: 'test-repo-restorer', path: 'test-repo-restorer') } + let!(:project) { create(:empty_project) } + let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } + let(:bundler) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) } + let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) } + let(:restorer) do + described_class.new(path_to_bundle: bundle_path, + shared: shared, + project: project) + end + + before do + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + + bundler.save + end + + after do + FileUtils.rm_rf(export_path) + FileUtils.rm_rf(project_with_repo.repository.path_to_repo) + FileUtils.rm_rf(project.repository.path_to_repo) + end + + it 'restores the repo successfully' do + expect(restorer.restore).to be true + end + + it 'has the webhooks' do + restorer.restore + + expect(Gitlab::Git::Hook.new('post-receive', project.repository.path_to_repo)).to exist + end + end +end diff --git a/spec/lib/gitlab/slash_commands/extractor_spec.rb b/spec/lib/gitlab/slash_commands/extractor_spec.rb index 1e4954c4af8dcb9cbb427c6ee3aa2dccb6fe16c6..d7f77486b3e6ebab612aed81d4b861d3e297fd45 100644 --- a/spec/lib/gitlab/slash_commands/extractor_spec.rb +++ b/spec/lib/gitlab/slash_commands/extractor_spec.rb @@ -81,6 +81,14 @@ describe Gitlab::SlashCommands::Extractor do let(:original_msg) { "/assign @joe\nworld" } let(:final_msg) { "world" } end + + it 'allows slash in command arguments' do + msg = "/assign @joe / @jane\nworld" + msg, commands = extractor.extract_commands(msg) + + expect(commands).to eq [['assign', '@joe / @jane']] + expect(msg).to eq 'world' + end end context 'in the middle of content' do diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb deleted file mode 100644 index 7a140518dd2448f348fa46907157d7caab6da44c..0000000000000000000000000000000000000000 --- a/spec/lib/gitlab/themes_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Themes, lib: true do - describe '.body_classes' do - it 'returns a space-separated list of class names' do - css = described_class.body_classes - - expect(css).to include('ui_graphite') - expect(css).to include(' ui_charcoal ') - expect(css).to include(' ui_blue') - end - end - - describe '.by_id' do - it 'returns a Theme by its ID' do - expect(described_class.by_id(1).name).to eq 'Graphite' - expect(described_class.by_id(6).name).to eq 'Blue' - end - end - - describe '.default' do - it 'returns the default application theme' do - allow(described_class).to receive(:default_id).and_return(2) - expect(described_class.default.id).to eq 2 - end - - it 'prevents an infinite loop when configuration default is invalid' do - default = described_class::APPLICATION_DEFAULT - themes = described_class::THEMES - - config = double(default_theme: 0).as_null_object - allow(Gitlab).to receive(:config).and_return(config) - expect(described_class.default.id).to eq default - - config = double(default_theme: themes.size + 5).as_null_object - allow(Gitlab).to receive(:config).and_return(config) - expect(described_class.default.id).to eq default - end - end - - describe '.each' do - it 'passes the block to the THEMES Array' do - ids = [] - described_class.each { |theme| ids << theme.id } - expect(ids).not_to be_empty - end - end -end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index a4edabfbc5b38690ff78357c39525786d769ab26..444ad40ae0dce8a343b57b8219c8b51332697ad3 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Ci::Build, :models do + let(:user) { create(:user) } let(:project) { create(:project, :repository) } let(:build) { create(:ci_build, pipeline: pipeline) } let(:test_trace) { 'This is a test' } @@ -213,14 +214,16 @@ describe Ci::Build, :models do end it 'expects to have retried builds instead the original ones' do - retried_rspec = Ci::Build.retry(rspec_test) - expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id) + project.add_developer(user) + + retried_rspec = Ci::Build.retry(rspec_test, user) + + expect(staging.depends_on_builds.map(&:id)) + .to contain_exactly(build.id, retried_rspec.id, rubocop_test.id) end end describe '#detailed_status' do - let(:user) { create(:user) } - it 'returns a detailed status' do expect(build.detailed_status(user)) .to be_a Gitlab::Ci::Status::Build::Cancelable @@ -819,12 +822,16 @@ describe Ci::Build, :models do subject { build.other_actions } + before do + project.add_developer(user) + end + it 'returns other actions' do is_expected.to contain_exactly(other_build) end context 'when build is retried' do - let!(:new_build) { Ci::Build.retry(build) } + let!(:new_build) { Ci::Build.retry(build, user) } it 'does not return any of them' do is_expected.not_to include(build, new_build) @@ -832,7 +839,7 @@ describe Ci::Build, :models do end context 'when other build is retried' do - let!(:retried_build) { Ci::Build.retry(other_build) } + let!(:retried_build) { Ci::Build.retry(other_build, user) } it 'returns a retried build' do is_expected.to contain_exactly(retried_build) @@ -863,21 +870,29 @@ describe Ci::Build, :models do describe '#play' do let(:build) { create(:ci_build, :manual, pipeline: pipeline) } - subject { build.play } + before do + project.add_developer(user) + end + + context 'when build is manual' do + it 'enqueues a build' do + new_build = build.play(user) - it 'enqueues a build' do - is_expected.to be_pending - is_expected.to eq(build) + expect(new_build).to be_pending + expect(new_build).to eq(build) + end end - context 'for successful build' do + context 'when build is passed' do before do build.update(status: 'success') end it 'creates a new build' do - is_expected.to be_pending - is_expected.not_to eq(build) + new_build = build.play(user) + + expect(new_build).to be_pending + expect(new_build).not_to eq(build) end end end @@ -1252,12 +1267,9 @@ describe Ci::Build, :models do end context 'when build has user' do - let(:user) { create(:user, username: 'starter') } let(:user_variables) do - [ - { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true }, - { key: 'GITLAB_USER_EMAIL', value: user.email, public: true } - ] + [ { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true }, + { key: 'GITLAB_USER_EMAIL', value: user.email, public: true } ] end before do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 426be74cd0209865a2f6ee1541bf580e39bb30c4..10c2bfbb4000ee0b6d6d98e42448c1725dc63be3 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -3,8 +3,12 @@ require 'spec_helper' describe Ci::Pipeline, models: true do include EmailHelpers - let(:project) { FactoryGirl.create :empty_project } - let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, status: 'created', project: project } + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + + let(:pipeline) do + create(:ci_empty_pipeline, status: :created, project: project) + end it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:user) } @@ -503,7 +507,9 @@ describe Ci::Pipeline, models: true do end describe '#status' do - let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') } + let(:build) do + create(:ci_build, :created, pipeline: pipeline, name: 'test') + end subject { pipeline.reload.status } @@ -545,13 +551,21 @@ describe Ci::Pipeline, models: true do build.cancel end - it { is_expected.to eq('canceled') } + context 'when build is pending' do + let(:build) do + create(:ci_build, :pending, pipeline: pipeline) + end + + it { is_expected.to eq('canceled') } + end end context 'on failure and build retry' do before do build.drop - Ci::Build.retry(build) + project.add_developer(user) + + Ci::Build.retry(build, user) end # We are changing a state: created > failed > running @@ -563,8 +577,6 @@ describe Ci::Pipeline, models: true do end describe '#detailed_status' do - let(:user) { create(:user) } - subject { pipeline.detailed_status(user) } context 'when pipeline is created' do @@ -720,7 +732,7 @@ describe Ci::Pipeline, models: true do describe '#cancel_running' do let(:latest_status) { pipeline.statuses.pluck(:status) } - context 'when there is a running external job and created build' do + context 'when there is a running external job and a regular job' do before do create(:ci_build, :running, pipeline: pipeline) create(:generic_commit_status, :running, pipeline: pipeline) @@ -733,7 +745,7 @@ describe Ci::Pipeline, models: true do end end - context 'when builds are in different stages' do + context 'when jobs are in different stages' do before do create(:ci_build, :running, stage_idx: 0, pipeline: pipeline) create(:ci_build, :running, stage_idx: 1, pipeline: pipeline) @@ -745,17 +757,34 @@ describe Ci::Pipeline, models: true do expect(latest_status).to contain_exactly('canceled', 'canceled') end end + + context 'when there are created builds present in the pipeline' do + before do + create(:ci_build, :running, stage_idx: 0, pipeline: pipeline) + create(:ci_build, :created, stage_idx: 1, pipeline: pipeline) + + pipeline.cancel_running + end + + it 'cancels created builds' do + expect(latest_status).to eq ['canceled', 'canceled'] + end + end end describe '#retry_failed' do let(:latest_status) { pipeline.statuses.latest.pluck(:status) } + before do + project.add_developer(user) + end + context 'when there is a failed build and failed external status' do before do create(:ci_build, :failed, name: 'build', pipeline: pipeline) create(:generic_commit_status, :failed, name: 'jenkins', pipeline: pipeline) - pipeline.retry_failed(create(:user)) + pipeline.retry_failed(user) end it 'retries only build' do @@ -768,11 +797,11 @@ describe Ci::Pipeline, models: true do create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline) create(:ci_build, :failed, name: 'jenkins', stage_idx: 1, pipeline: pipeline) - pipeline.retry_failed(create(:user)) + pipeline.retry_failed(user) end it 'retries both builds' do - expect(latest_status).to contain_exactly('pending', 'pending') + expect(latest_status).to contain_exactly('pending', 'created') end end @@ -781,11 +810,11 @@ describe Ci::Pipeline, models: true do create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline) create(:ci_build, :canceled, name: 'jenkins', stage_idx: 1, pipeline: pipeline) - pipeline.retry_failed(create(:user)) + pipeline.retry_failed(user) end it 'retries both builds' do - expect(latest_status).to contain_exactly('pending', 'pending') + expect(latest_status).to contain_exactly('pending', 'created') end end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 3f32248e52b0d9357a50cee55c58fcf642e5425c..f8513ac8b1c1ba6d134a670ba40313c4ee95b7aa 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -290,7 +290,7 @@ describe Ci::Runner, models: true do let!(:last_update) { runner.ensure_runner_queue_value } before do - runner.update(description: 'new runner') + Ci::UpdateRunnerService.new(runner).update(description: 'new runner') end it 'sets a new last_update value' do @@ -318,6 +318,25 @@ describe Ci::Runner, models: true do end end + describe '#destroy' do + let(:runner) { create(:ci_runner) } + + context 'when there is a tick in the queue' do + let!(:queue_key) { runner.send(:runner_queue_key) } + + before do + runner.tick_runner_queue + runner.destroy + end + + it 'cleans up the queue' do + Gitlab::Redis.with do |redis| + expect(redis.get(queue_key)).to be_nil + end + end + end + end + describe '.assignable_for' do let(:runner) { create(:ci_runner) } let(:project) { create(:empty_project) } diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index bf4394f7d5b758644934ab3478cd9a6c0739704c..36533bdd11e32103c0bfcabafee19eac4fec896a 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe CommitStatus, models: true do +describe CommitStatus, :models do let(:project) { create(:project, :repository) } let(:pipeline) do @@ -127,7 +127,7 @@ describe CommitStatus, models: true do end describe '.latest' do - subject { CommitStatus.latest.order(:id) } + subject { described_class.latest.order(:id) } let(:statuses) do [create_status(name: 'aa', ref: 'bb', status: 'running'), @@ -143,7 +143,7 @@ describe CommitStatus, models: true do end describe '.running_or_pending' do - subject { CommitStatus.running_or_pending.order(:id) } + subject { described_class.running_or_pending.order(:id) } let(:statuses) do [create_status(name: 'aa', ref: 'bb', status: 'running'), @@ -159,7 +159,21 @@ describe CommitStatus, models: true do end describe '.exclude_ignored' do - subject { CommitStatus.exclude_ignored.order(:id) } + subject { described_class.after_stage(0) } + + let(:statuses) do + [create_status(name: 'aa', stage_idx: 0), + create_status(name: 'cc', stage_idx: 1), + create_status(name: 'aa', stage_idx: 2)] + end + + it 'returns statuses from second and third stage' do + is_expected.to eq(statuses.values_at(1, 2)) + end + end + + describe '.exclude_ignored' do + subject { described_class.exclude_ignored.order(:id) } let(:statuses) do [create_status(when: 'manual', status: 'skipped'), diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb index 32935bc0b09343404f7db11ea92c52a544d60588..b6e5c95d18a4614c4bc0967ed23c9de841354db7 100644 --- a/spec/models/concerns/spammable_spec.rb +++ b/spec/models/concerns/spammable_spec.rb @@ -14,8 +14,9 @@ describe Issue, 'Spammable' do end describe 'InstanceMethods' do + let(:issue) { build(:issue, spam: true) } + it 'should be invalid if spam' do - issue = build(:issue, spam: true) expect(issue.valid?).to be_falsey end @@ -29,5 +30,20 @@ describe Issue, 'Spammable' do expect(issue.check_for_spam?).to eq(false) end end + + describe '#submittable_as_spam_by?' do + let(:admin) { build(:admin) } + let(:user) { build(:user) } + + before do + allow(issue).to receive(:submittable_as_spam?).and_return(true) + end + + it 'tests if the user can submit spam' do + expect(issue.submittable_as_spam_by?(admin)).to be(true) + expect(issue.submittable_as_spam_by?(user)).to be(false) + expect(issue.submittable_as_spam_by?(nil)).to be_nil + end + end end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 960f29f3805112c0137af9869111743c7756fd3b..f0ed0c679d5acdaf6820b499c9101ecae96129c5 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -155,7 +155,7 @@ describe Environment, models: true do end describe '#stop_with_action!' do - let(:user) { create(:user) } + let(:user) { create(:admin) } subject { environment.stop_with_action!(user) } diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb index 50ad5013df94e8c541cf4ce594d6436fa6d8a11c..3bd7ec18ae0b8fa70995618e8d9544eda585c261 100644 --- a/spec/models/project_services/chat_message/build_message_spec.rb +++ b/spec/models/project_services/chat_message/build_message_spec.rb @@ -11,21 +11,28 @@ describe ChatMessage::BuildMessage do project_name: 'project_name', project_url: 'http://example.gitlab.com', + build_id: 1, + build_name: build_name, + build_stage: stage, commit: { status: status, author_name: 'hacker', + author_url: 'http://example.gitlab.com/hacker', duration: duration, }, } end let(:message) { build_message } + let(:stage) { 'test' } + let(:status) { 'success' } + let(:build_name) { 'rspec' } + let(:duration) { 10 } context 'build succeeded' do let(:status) { 'success' } let(:color) { 'good' } - let(:duration) { 10 } let(:message) { build_message('passed') } it 'returns a message with information about succeeded build' do @@ -38,7 +45,6 @@ describe ChatMessage::BuildMessage do context 'build failed' do let(:status) { 'failed' } let(:color) { 'danger' } - let(:duration) { 10 } it 'returns a message with information about failed build' do expect(subject.pretext).to be_empty @@ -47,11 +53,25 @@ describe ChatMessage::BuildMessage do end end - def build_message(status_text = status) + it 'returns a message with information on build' do + expect(subject.fallback).to include("on build <http://example.gitlab.com/builds/1|#{build_name}>") + end + + it 'returns a message with stage name' do + expect(subject.fallback).to include("of stage #{stage}") + end + + it 'returns a message with link to author' do + expect(subject.fallback).to include("by <http://example.gitlab.com/hacker|hacker>") + end + + def build_message(status_text = status, stage_text = stage, build_text = build_name) "<http://example.gitlab.com|project_name>:" \ " Commit <http://example.gitlab.com/commit/" \ "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \ " of <http://example.gitlab.com/commits/develop|develop> branch" \ - " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" + " by <http://example.gitlab.com/hacker|hacker> #{status_text}" \ + " on build <http://example.gitlab.com/builds/1|#{build_text}>" \ + " of stage #{stage_text} in #{duration} #{'second'.pluralize(duration)}" end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 89cef7ab978154ecf2a59a9328fb4d2abbeededc..584a4facd94a5d0e0098d64a987d56a5134db449 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -582,18 +582,16 @@ describe User, models: true do it "applies defaults to user" do expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit) expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group) - expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme) expect(user.external).to be_falsey end end describe 'with default overrides' do - let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true, theme_id: 1) } + let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true) } it "applies defaults to user" do expect(user.projects_limit).to eq(123) expect(user.can_create_group).to be_falsey - expect(user.theme_id).to eq(1) end end diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb index e487297748b1dbfe30c08e79e9ce0f705255a112..919c98d6437a05239948718637746eb8e74140a8 100644 --- a/spec/requests/api/access_requests_spec.rb +++ b/spec/requests/api/access_requests_spec.rb @@ -48,6 +48,7 @@ describe API::AccessRequests, api: true do get api("/#{source_type.pluralize}/#{source.id}/access_requests", master) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) end diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index c8e8f31cc1f4390167c892c2d9cbdd517506cd55..6cc1ef315db895646741cb4dccb5daa1b201a5f0 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -34,6 +34,7 @@ describe API::AwardEmoji, api: true do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['name']).to eq(downvote.name) end diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb index c14c3cb1ce708594497a6830067e843d257f97c1..71df534ebe15e7e9d90b6412209694d05ea7ce47 100644 --- a/spec/requests/api/boards_spec.rb +++ b/spec/requests/api/boards_spec.rb @@ -55,6 +55,7 @@ describe API::Boards, api: true do get api(base_url, user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(board.id) @@ -72,6 +73,7 @@ describe API::Boards, api: true do get api(base_url, user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['label']['name']).to eq(dev_label.title) diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 3e66236f6aef380fee172f00b5b017f76b75b5c6..2e6db0f43c6ecd8252d2c0cb467c9e56124f185e 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -17,8 +17,10 @@ describe API::Branches, api: true do it "returns an array of project branches" do project.repository.expire_all_method_caches - get api("/projects/#{project.id}/repository/branches", user) + get api("/projects/#{project.id}/repository/branches", user), per_page: 100 + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array branch_names = json_response.map { |x| x['name'] } expect(branch_names).to match_array(project.repository.branch_names) diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb index 7c9078b28642cd1d44b9675368ffc7152659c40a..921d8714173a2f6636959c70d3f49d9190eb3e02 100644 --- a/spec/requests/api/broadcast_messages_spec.rb +++ b/spec/requests/api/broadcast_messages_spec.rb @@ -25,6 +25,7 @@ describe API::BroadcastMessages, api: true do get api('/broadcast_messages', admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_kind_of(Array) expect(json_response.first.keys) .to match_array(%w(id message starts_at ends_at color font active)) diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 834c4e52693f133c340069f0b031d41052fbde17..38aef7f276767278f933c725b8ed26154f495657 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -22,6 +22,7 @@ describe API::Builds, api: true do context 'authorized user' do it 'returns project builds' do expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array end @@ -97,6 +98,7 @@ describe API::Builds, api: true do it 'returns project jobs for specific commit' do expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq 2 end diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index eb53fd718721d6c91e9ff3633a709a80d6b17a7f..81a8856b8f1154c9f7b5b22396a7c37ec7cb8824 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -5,12 +5,15 @@ describe API::CommitStatuses, api: true do let!(:project) { create(:project, :repository) } let(:commit) { project.repository.commit } - let(:commit_status) { create(:commit_status, pipeline: pipeline) } let(:guest) { create_user(:guest) } let(:reporter) { create_user(:reporter) } let(:developer) { create_user(:developer) } let(:sha) { commit.id } + let(:commit_status) do + create(:commit_status, status: :pending, pipeline: pipeline) + end + describe "GET /projects/:id/repository/commits/:sha/statuses" do let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" } @@ -18,10 +21,6 @@ describe API::CommitStatuses, api: true do let!(:master) { project.pipelines.create(sha: commit.id, ref: 'master') } let!(:develop) { project.pipelines.create(sha: commit.id, ref: 'develop') } - it_behaves_like 'a paginated resources' do - let(:request) { get api(get_url, reporter) } - end - context "reporter user" do let(:statuses_id) { json_response.map { |status| status['id'] } } @@ -42,6 +41,7 @@ describe API::CommitStatuses, api: true do it 'returns latest commit statuses' do expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(statuses_id).to contain_exactly(status3.id, status4.id, status5.id, status6.id) json_response.sort_by!{ |status| status['id'] } @@ -54,7 +54,7 @@ describe API::CommitStatuses, api: true do it 'returns all commit statuses' do expect(response).to have_http_status(200) - + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(statuses_id).to contain_exactly(status1.id, status2.id, status3.id, status4.id, @@ -67,7 +67,7 @@ describe API::CommitStatuses, api: true do it 'returns latest commit statuses for specific ref' do expect(response).to have_http_status(200) - + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(statuses_id).to contain_exactly(status3.id, status5.id) end @@ -78,7 +78,7 @@ describe API::CommitStatuses, api: true do it 'return latest commit statuses for specific name' do expect(response).to have_http_status(200) - + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(statuses_id).to contain_exactly(status4.id, status5.id) end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 3d0d673535916d864ae60d93a7722c838d9bae61..ecc6a59786930d47233659987f538fd003ebc80a 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -72,7 +72,7 @@ describe API::Commits, api: true do get api("/projects/#{project.id}/repository/commits?since=invalid-date", user) expect(response).to have_http_status(400) - expect(json_response['message']).to include "\"since\" must be a timestamp in ISO 8601 format" + expect(json_response['error']).to eq('since is invalid') end end @@ -456,6 +456,7 @@ describe API::Commits, api: true do it 'returns merge_request comments' do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['note']).to eq('a comment on a commit') @@ -474,6 +475,20 @@ describe API::Commits, api: true do expect(response).to have_http_status(401) end end + + context 'when the commit is present on two projects' do + let(:forked_project) { create(:project, :repository, creator: user2, namespace: user2.namespace) } + let!(:forked_project_note) { create(:note_on_commit, author: user2, project: forked_project, commit_id: forked_project.repository.commit.id, note: 'a comment on a commit for fork') } + + it 'returns the comments for the target project' do + get api("/projects/#{forked_project.id}/repository/commits/#{forked_project.repository.commit.id}/comments", user2) + + expect(response).to have_http_status(200) + expect(json_response.length).to eq(1) + expect(json_response.first['note']).to eq('a comment on a commit for fork') + expect(json_response.first['author']['id']).to eq(user2.id) + end + end end describe 'POST :id/repository/commits/:sha/cherry_pick' do diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 766234d7104d0cd2f99451df9e979603e16fa90f..7e682e91bd1a70e45d53807ac4727e37d86f305e 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -35,6 +35,7 @@ describe API::DeployKeys, api: true do get api('/deploy_keys', admin) expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id) end @@ -48,6 +49,7 @@ describe API::DeployKeys, api: true do get api("/projects/#{project.id}/deploy_keys", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['title']).to eq(deploy_key.title) end @@ -146,25 +148,4 @@ describe API::DeployKeys, api: true do end end end - - describe 'DELETE /projects/:id/deploy_keys/:key_id/disable' do - context 'when the user can admin the project' do - it 'disables the key' do - expect do - delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", admin) - end.to change { project.deploy_keys.count }.from(1).to(0) - - expect(response).to have_http_status(200) - expect(json_response['id']).to eq(deploy_key.id) - end - end - - context 'when authenticated as non-admin user' do - it 'should return a 404 error' do - delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", user) - - expect(response).to have_http_status(404) - end - end - end end diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index 31e3cfa1b2ff7f0c273f514058a6c61fb7d95be9..e55575ffbda766faca0dd29c93c1df56bb757f75 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -14,14 +14,11 @@ describe API::Deployments, api: true do describe 'GET /projects/:id/deployments' do context 'as member of the project' do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/projects/#{project.id}/deployments", user) } - end - it 'returns projects deployments' do get api("/projects/#{project.id}/deployments", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) expect(json_response.first['iid']).to eq(deployment.iid) diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 8168b6137668917fb5f248a684b12a5bc91d4903..d0958d39d44efa3b5e772c1cf9107c131d9524e0 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -14,14 +14,11 @@ describe API::Environments, api: true do describe 'GET /projects/:id/environments' do context 'as member of the project' do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/projects/#{project.id}/environments", user) } - end - it 'returns project environments' do get api("/projects/#{project.id}/environments", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) expect(json_response.first['name']).to eq(environment.name) diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index ccd7898586ca859342d560788f89b77f898ddfaf..a59112579e5b198c967d2537f26d183a7de61a6a 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -33,6 +33,7 @@ describe API::Groups, api: true do get api("/groups", user1) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response) @@ -43,6 +44,7 @@ describe API::Groups, api: true do get api("/groups", user1), statistics: true expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first).not_to include 'statistics' end @@ -53,6 +55,7 @@ describe API::Groups, api: true do get api("/groups", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) end @@ -61,6 +64,7 @@ describe API::Groups, api: true do get api("/groups", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first).not_to include('statistics') end @@ -78,6 +82,7 @@ describe API::Groups, api: true do get api("/groups", admin), statistics: true expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response) .to satisfy_one { |group| group['statistics'] == attributes } @@ -89,6 +94,7 @@ describe API::Groups, api: true do get api("/groups", admin), skip_groups: [group2.id] expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) end @@ -103,6 +109,7 @@ describe API::Groups, api: true do get api("/groups", user1), all_available: true expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_groups).to contain_exactly(public_group.name, group1.name) end @@ -120,6 +127,7 @@ describe API::Groups, api: true do get api("/groups", user1) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_groups).to eq([group3.name, group1.name]) end @@ -128,6 +136,7 @@ describe API::Groups, api: true do get api("/groups", user1), sort: "desc" expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_groups).to eq([group1.name, group3.name]) end @@ -136,6 +145,7 @@ describe API::Groups, api: true do get api("/groups", user1), order_by: "path" expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_groups).to eq([group1.name, group3.name]) end @@ -156,6 +166,7 @@ describe API::Groups, api: true do get api('/groups/owned', user2) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['name']).to eq(group2.name) end @@ -290,6 +301,7 @@ describe API::Groups, api: true do get api("/groups/#{group1.id}/projects", user1) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response.length).to eq(2) project_names = json_response.map { |proj| proj['name' ] } expect(project_names).to match_array([project1.name, project3.name]) @@ -300,6 +312,7 @@ describe API::Groups, api: true do get api("/groups/#{group1.id}/projects", user1), simple: true expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response.length).to eq(2) project_names = json_response.map { |proj| proj['name' ] } expect(project_names).to match_array([project1.name, project3.name]) @@ -312,6 +325,7 @@ describe API::Groups, api: true do get api("/groups/#{group1.id}/projects", user1), visibility: 'public' expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an(Array) expect(json_response.length).to eq(1) expect(json_response.first['name']).to eq(public_project.name) @@ -335,6 +349,7 @@ describe API::Groups, api: true do get api("/groups/#{group1.id}/projects", user3) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response.length).to eq(1) expect(json_response.first['name']).to eq(project3.name) end @@ -365,6 +380,7 @@ describe API::Groups, api: true do get api("/groups/#{group2.id}/projects", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response.length).to eq(1) expect(json_response.first['name']).to eq(project2.name) end @@ -381,6 +397,7 @@ describe API::Groups, api: true do get api("/groups/#{group1.path}/projects", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers project_names = json_response.map { |proj| proj['name' ] } expect(project_names).to match_array([project1.name, project3.name]) end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index cca00df95919271bc044eabb657f9dc0689bccbc..74ac7955cb8f89d97987bf935c878ee79cec7d65 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -68,7 +68,9 @@ describe API::Issues, api: true do context "when authenticated" do it "returns an array of issues" do get api("/issues", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['title']).to eq(issue.title) expect(json_response.last).to have_key('web_url') @@ -76,7 +78,9 @@ describe API::Issues, api: true do it 'returns an array of closed issues' do get api('/issues?state=closed', user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(closed_issue.id) @@ -84,7 +88,9 @@ describe API::Issues, api: true do it 'returns an array of opened issues' do get api('/issues?state=opened', user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(issue.id) @@ -92,7 +98,9 @@ describe API::Issues, api: true do it 'returns an array of all issues' do get api('/issues?state=all', user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['id']).to eq(issue.id) @@ -101,7 +109,9 @@ describe API::Issues, api: true do it 'returns an array of labeled issues' do get api("/issues?labels=#{label.title}", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([label.title]) @@ -111,6 +121,7 @@ describe API::Issues, api: true do get api("/issues?labels=#{label.title},foo,bar", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([label.title]) @@ -118,14 +129,18 @@ describe API::Issues, api: true do it 'returns an empty array if no issue matches labels' do get api('/issues?labels=foo,bar', user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end it 'returns an array of labeled issues matching given state' do get api("/issues?labels=#{label.title}&state=opened", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([label.title]) @@ -134,7 +149,9 @@ describe API::Issues, api: true do it 'returns an empty array if no issue matches labels and state filters' do get api("/issues?labels=#{label.title}&state=closed", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -143,6 +160,7 @@ describe API::Issues, api: true do get api("/issues?milestone=#{empty_milestone.title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -151,6 +169,7 @@ describe API::Issues, api: true do get api("/issues?milestone=foo", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -159,6 +178,7 @@ describe API::Issues, api: true do get api("/issues?milestone=#{milestone.title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['id']).to eq(issue.id) @@ -170,6 +190,7 @@ describe API::Issues, api: true do '&state=closed', user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(closed_issue.id) @@ -179,6 +200,7 @@ describe API::Issues, api: true do get api("/issues?milestone=#{no_milestone_title}", author) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(confidential_issue.id) @@ -186,36 +208,40 @@ describe API::Issues, api: true do it 'sorts by created_at descending by default' do get api('/issues', user) - response_dates = json_response.map { |issue| issue['created_at'] } + response_dates = json_response.map { |issue| issue['created_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort.reverse) end it 'sorts ascending when requested' do get api('/issues?sort=asc', user) - response_dates = json_response.map { |issue| issue['created_at'] } + response_dates = json_response.map { |issue| issue['created_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort) end it 'sorts by updated_at descending when requested' do get api('/issues?order_by=updated_at', user) - response_dates = json_response.map { |issue| issue['updated_at'] } + response_dates = json_response.map { |issue| issue['updated_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort.reverse) end it 'sorts by updated_at ascending when requested' do get api('/issues?order_by=updated_at&sort=asc', user) - response_dates = json_response.map { |issue| issue['updated_at'] } + response_dates = json_response.map { |issue| issue['updated_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort) end @@ -269,6 +295,7 @@ describe API::Issues, api: true do get api(base_url, non_member) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['title']).to eq(group_issue.title) @@ -278,6 +305,7 @@ describe API::Issues, api: true do get api(base_url, author) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) end @@ -286,6 +314,7 @@ describe API::Issues, api: true do get api(base_url, assignee) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) end @@ -294,6 +323,7 @@ describe API::Issues, api: true do get api(base_url, user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) end @@ -302,6 +332,7 @@ describe API::Issues, api: true do get api(base_url, admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) end @@ -310,6 +341,7 @@ describe API::Issues, api: true do get api("#{base_url}?labels=#{group_label.title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([group_label.title]) @@ -319,6 +351,7 @@ describe API::Issues, api: true do get api("#{base_url}?labels=#{group_label.title},foo,bar", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -327,6 +360,7 @@ describe API::Issues, api: true do get api("#{base_url}?labels=foo,bar", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -335,6 +369,7 @@ describe API::Issues, api: true do get api("#{base_url}?milestone=#{group_empty_milestone.title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -343,6 +378,7 @@ describe API::Issues, api: true do get api("#{base_url}?milestone=foo", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -351,6 +387,7 @@ describe API::Issues, api: true do get api("#{base_url}?milestone=#{group_milestone.title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(group_issue.id) @@ -361,6 +398,7 @@ describe API::Issues, api: true do '&state=closed', user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(group_closed_issue.id) @@ -370,6 +408,7 @@ describe API::Issues, api: true do get api("#{base_url}?milestone=#{no_milestone_title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(group_confidential_issue.id) @@ -377,36 +416,40 @@ describe API::Issues, api: true do it 'sorts by created_at descending by default' do get api(base_url, user) - response_dates = json_response.map { |issue| issue['created_at'] } + response_dates = json_response.map { |issue| issue['created_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort.reverse) end it 'sorts ascending when requested' do get api("#{base_url}?sort=asc", user) - response_dates = json_response.map { |issue| issue['created_at'] } + response_dates = json_response.map { |issue| issue['created_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort) end it 'sorts by updated_at descending when requested' do get api("#{base_url}?order_by=updated_at", user) - response_dates = json_response.map { |issue| issue['updated_at'] } + response_dates = json_response.map { |issue| issue['updated_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort.reverse) end it 'sorts by updated_at ascending when requested' do get api("#{base_url}?order_by=updated_at&sort=asc", user) - response_dates = json_response.map { |issue| issue['updated_at'] } + response_dates = json_response.map { |issue| issue['updated_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort) end @@ -430,12 +473,17 @@ describe API::Issues, api: true do get api("/projects/#{restricted_project.id}/issues", non_member) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response).to eq([]) end it 'returns project issues without confidential issues for non project members' do get api("#{base_url}/issues", non_member) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['title']).to eq(issue.title) @@ -443,7 +491,9 @@ describe API::Issues, api: true do it 'returns project issues without confidential issues for project members with guest role' do get api("#{base_url}/issues", guest) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['title']).to eq(issue.title) @@ -451,7 +501,9 @@ describe API::Issues, api: true do it 'returns project confidential issues for author' do get api("#{base_url}/issues", author) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.first['title']).to eq(issue.title) @@ -459,7 +511,9 @@ describe API::Issues, api: true do it 'returns project confidential issues for assignee' do get api("#{base_url}/issues", assignee) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.first['title']).to eq(issue.title) @@ -467,7 +521,9 @@ describe API::Issues, api: true do it 'returns project issues with confidential issues for project members' do get api("#{base_url}/issues", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.first['title']).to eq(issue.title) @@ -475,7 +531,9 @@ describe API::Issues, api: true do it 'returns project confidential issues for admin' do get api("#{base_url}/issues", admin) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.first['title']).to eq(issue.title) @@ -483,7 +541,9 @@ describe API::Issues, api: true do it 'returns an array of labeled project issues' do get api("#{base_url}/issues?labels=#{label.title}", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([label.title]) @@ -493,6 +553,7 @@ describe API::Issues, api: true do get api("#{base_url}/issues?labels=#{label.title},foo,bar", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([label.title]) @@ -500,21 +561,27 @@ describe API::Issues, api: true do it 'returns an empty array if no project issue matches labels' do get api("#{base_url}/issues?labels=foo,bar", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end it 'returns an empty array if no issue matches milestone' do get api("#{base_url}/issues?milestone=#{empty_milestone.title}", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end it 'returns an empty array if milestone does not exist' do get api("#{base_url}/issues?milestone=foo", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -523,6 +590,7 @@ describe API::Issues, api: true do get api("#{base_url}/issues?milestone=#{milestone.title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['id']).to eq(issue.id) @@ -530,9 +598,10 @@ describe API::Issues, api: true do end it 'returns an array of issues matching state in milestone' do - get api("#{base_url}/issues?milestone=#{milestone.title}"\ - '&state=closed', user) + get api("#{base_url}/issues?milestone=#{milestone.title}&state=closed", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(closed_issue.id) @@ -542,6 +611,7 @@ describe API::Issues, api: true do get api("#{base_url}/issues?milestone=#{no_milestone_title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(confidential_issue.id) @@ -549,36 +619,40 @@ describe API::Issues, api: true do it 'sorts by created_at descending by default' do get api("#{base_url}/issues", user) - response_dates = json_response.map { |issue| issue['created_at'] } + response_dates = json_response.map { |issue| issue['created_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort.reverse) end it 'sorts ascending when requested' do get api("#{base_url}/issues?sort=asc", user) - response_dates = json_response.map { |issue| issue['created_at'] } + response_dates = json_response.map { |issue| issue['created_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort) end it 'sorts by updated_at descending when requested' do get api("#{base_url}/issues?order_by=updated_at", user) - response_dates = json_response.map { |issue| issue['updated_at'] } + response_dates = json_response.map { |issue| issue['updated_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort.reverse) end it 'sorts by updated_at ascending when requested' do get api("#{base_url}/issues?order_by=updated_at&sort=asc", user) - response_dates = json_response.map { |issue| issue['updated_at'] } + response_dates = json_response.map { |issue| issue['updated_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort) end diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index a8cd787f3983807a3bb82eca6122ff8cc7ffbe1d..5d7a76cf3beb5cc7a4cd95507e31d92e423a6968 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -30,6 +30,7 @@ describe API::Labels, api: true do get api("/projects/#{project.id}/labels", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(3) expect(json_response.first.keys).to match_array expected_keys diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 3e9bcfd1a6014296de42b8e1a4ace0e451d77a2b..31166b50033d850542d1734c004a75638ee3daed 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -34,9 +34,12 @@ describe API::Members, api: true do context "when authenticated as a #{type}" do it 'returns 200' do user = public_send(type) + get api("/#{source_type.pluralize}/#{source.id}/members", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(2) expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] end @@ -49,6 +52,8 @@ describe API::Members, api: true do get api("/#{source_type.pluralize}/#{source.id}/members", developer) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(2) expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] end @@ -57,6 +62,8 @@ describe API::Members, api: true do get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.count).to eq(1) expect(json_response.first['username']).to eq(master.username) end diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb index e1887138aab527ced9ea697c37f414994f11ebaf..1d02e827183393734cf2814e4d3656a56bf71427 100644 --- a/spec/requests/api/merge_request_diffs_spec.rb +++ b/spec/requests/api/merge_request_diffs_spec.rb @@ -19,6 +19,8 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do merge_request_diff = merge_request.merge_request_diffs.first expect(response.status).to eq 200 + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(merge_request.merge_request_diffs.size) expect(json_response.first['id']).to eq(merge_request_diff.id) expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index ff10e79e4174fb286ad696041686330e0a0c1acc..f4dee4a4ca1dffb5be9418fa4dcd0ea2ac87c070 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -27,7 +27,9 @@ describe API::MergeRequests, api: true do context "when authenticated" do it "returns an array of all merge_requests" do get api("/projects/#{project.id}/merge_requests", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.last['title']).to eq(merge_request.title) @@ -43,7 +45,9 @@ describe API::MergeRequests, api: true do it "returns an array of all merge_requests" do get api("/projects/#{project.id}/merge_requests?state", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.last['title']).to eq(merge_request.title) @@ -51,7 +55,9 @@ describe API::MergeRequests, api: true do it "returns an array of open merge_requests" do get api("/projects/#{project.id}/merge_requests?state=opened", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.last['title']).to eq(merge_request.title) @@ -59,7 +65,9 @@ describe API::MergeRequests, api: true do it "returns an array of closed merge_requests" do get api("/projects/#{project.id}/merge_requests?state=closed", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['title']).to eq(merge_request_closed.title) @@ -67,7 +75,9 @@ describe API::MergeRequests, api: true do it "returns an array of merged merge_requests" do get api("/projects/#{project.id}/merge_requests?state=merged", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['title']).to eq(merge_request_merged.title) @@ -91,7 +101,9 @@ describe API::MergeRequests, api: true do it "returns an array of merge_requests in ascending order" do get api("/projects/#{project.id}/merge_requests?sort=asc", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) response_dates = json_response.map{ |merge_request| merge_request['created_at'] } @@ -100,7 +112,9 @@ describe API::MergeRequests, api: true do it "returns an array of merge_requests in descending order" do get api("/projects/#{project.id}/merge_requests?sort=desc", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) response_dates = json_response.map{ |merge_request| merge_request['created_at'] } @@ -109,7 +123,9 @@ describe API::MergeRequests, api: true do it "returns an array of merge_requests ordered by updated_at" do get api("/projects/#{project.id}/merge_requests?order_by=updated_at", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) response_dates = json_response.map{ |merge_request| merge_request['updated_at'] } @@ -118,7 +134,9 @@ describe API::MergeRequests, api: true do it "returns an array of merge_requests ordered by created_at" do get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) response_dates = json_response.map{ |merge_request| merge_request['created_at'] } @@ -191,6 +209,8 @@ describe API::MergeRequests, api: true do commit = merge_request.commits.first expect(response.status).to eq 200 + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(merge_request.commits.size) expect(json_response.first['id']).to eq(commit.id) expect(json_response.first['title']).to eq(commit.title) @@ -205,6 +225,7 @@ describe API::MergeRequests, api: true do describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do it 'returns the change information of the merge_request' do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user) + expect(response.status).to eq 200 expect(json_response['changes'].size).to eq(merge_request.diffs.size) end @@ -572,7 +593,9 @@ describe API::MergeRequests, api: true do it "returns merge_request comments ordered by created_at" do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['note']).to eq("a comment on a MR") @@ -594,7 +617,9 @@ describe API::MergeRequests, api: true do end get api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(issue.id) @@ -602,7 +627,9 @@ describe API::MergeRequests, api: true do it 'returns an empty array when there are no issues to be closed' do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -616,6 +643,7 @@ describe API::MergeRequests, api: true do get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['title']).to eq(issue.title) diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index 8beef821d6c257aeeba8e17b34692dd53bc47778..418bf5a507cfa122efbcb387c94dc195a0a7a6fc 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -14,6 +14,7 @@ describe API::Milestones, api: true do get api("/projects/#{project.id}/milestones", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['title']).to eq(milestone.title) end @@ -28,6 +29,7 @@ describe API::Milestones, api: true do get api("/projects/#{project.id}/milestones?state=active", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(milestone.id) @@ -37,25 +39,18 @@ describe API::Milestones, api: true do get api("/projects/#{project.id}/milestones?state=closed", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(closed_milestone.id) end - end - - describe 'GET /projects/:id/milestones/:milestone_id' do - it 'returns a project milestone by id' do - get api("/projects/#{project.id}/milestones/#{milestone.id}", user) - - expect(response).to have_http_status(200) - expect(json_response['title']).to eq(milestone.title) - expect(json_response['iid']).to eq(milestone.iid) - end it 'returns a project milestone by iid' do get api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user) expect(response.status).to eq 200 + expect(response).to include_pagination_headers + expect(json_response.size).to eq(1) expect(json_response.size).to eq(1) expect(json_response.first['title']).to eq closed_milestone.title expect(json_response.first['id']).to eq closed_milestone.id @@ -70,6 +65,26 @@ describe API::Milestones, api: true do expect(json_response.first['id']).to eq milestone.id end + it 'returns a project milestone by iid array' do + get api("/projects/#{project.id}/milestones", user), iid: [milestone.iid, closed_milestone.iid] + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response.size).to eq(2) + expect(json_response.first['title']).to eq milestone.title + expect(json_response.first['id']).to eq milestone.id + end + end + + describe 'GET /projects/:id/milestones/:milestone_id' do + it 'returns a project milestone by id' do + get api("/projects/#{project.id}/milestones/#{milestone.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(milestone.title) + expect(json_response['iid']).to eq(milestone.iid) + end + it 'returns 401 error if user not authenticated' do get api("/projects/#{project.id}/milestones/#{milestone.id}") @@ -177,6 +192,7 @@ describe API::Milestones, api: true do get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['milestone']['title']).to eq(milestone.title) end @@ -202,6 +218,7 @@ describe API::Milestones, api: true do get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(2) expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id) @@ -214,6 +231,7 @@ describe API::Milestones, api: true do get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) expect(json_response.map { |issue| issue['id'] }).to include(issue.id) @@ -223,10 +241,47 @@ describe API::Milestones, api: true do get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", create(:user)) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) expect(json_response.map { |issue| issue['id'] }).to include(issue.id) end end end + + describe 'GET /projects/:id/milestones/:milestone_id/merge_requests' do + let(:merge_request) { create(:merge_request, source_project: project) } + before do + milestone.merge_requests << merge_request + end + + it 'returns project merge_requests for a particular milestone' do + get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['title']).to eq(merge_request.title) + expect(json_response.first['milestone']['title']).to eq(milestone.title) + end + + it 'returns a 404 error if milestone id not found' do + get api("/projects/#{project.id}/milestones/1234/merge_requests", user) + + expect(response).to have_http_status(404) + end + + it 'returns a 404 if the user has no access to the milestone' do + new_user = create :user + get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", new_user) + + expect(response).to have_http_status(404) + end + + it 'returns a 401 error if user not authenticated' do + get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests") + + expect(response).to have_http_status(401) + end + end end diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index a945d56f529ee1b94a0c31b1e4821fb877515c8f..da8fa06d0af95ea7bade62b960ec18c2b9a72969 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -18,17 +18,19 @@ describe API::Namespaces, api: true do context "when authenticated as admin" do it "admin: returns an array of all namespaces" do get api("/namespaces", admin) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(Namespace.count) end it "admin: returns an array of matched namespaces" do get api("/namespaces?search=#{group2.name}", admin) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(1) expect(json_response.last['path']).to eq(group2.path) expect(json_response.last['full_path']).to eq(group2.full_path) @@ -38,17 +40,19 @@ describe API::Namespaces, api: true do context "when authenticated as a regular user" do it "user: returns an array of namespaces" do get api("/namespaces", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(1) end it "admin: returns an array of matched namespaces" do get api("/namespaces?search=#{user.username}", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(1) end end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 0353ebea9e59b3c518ea17bcf01a98e24c0f3aa1..3cca4468be71d43ea85b28cd62263ca96898c72a 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -32,15 +32,12 @@ describe API::Notes, api: true do before { project.team << [user, :reporter] } describe "GET /projects/:id/noteable/:noteable_id/notes" do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) } - end - context "when noteable is an Issue" do it "returns an array of issue notes" do get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['body']).to eq(issue_note.note) end @@ -56,6 +53,7 @@ describe API::Notes, api: true do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response).to be_empty end @@ -75,6 +73,7 @@ describe API::Notes, api: true do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['body']).to eq(cross_reference_note.note) end @@ -87,6 +86,7 @@ describe API::Notes, api: true do get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['body']).to eq(snippet_note.note) end @@ -109,6 +109,7 @@ describe API::Notes, api: true do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['body']).to eq(merge_request_note.note) end diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index b7a0b5a9e1302e6b5be4913491e81d1a43597665..98d004b572e2dc2afd104d57ef1935d182d35076 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -15,15 +15,12 @@ describe API::Pipelines, api: true do before { project.team << [user, :master] } describe 'GET /projects/:id/pipelines ' do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/projects/#{project.id}/pipelines", user) } - end - context 'authorized user' do it 'returns project pipelines' do get api("/projects/#{project.id}/pipelines", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['sha']).to match /\A\h{40}\z/ expect(json_response.first['id']).to eq pipeline.id diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index f4973d7108859a33a8823a5434593ade1d666601..20c76bd2c057611373e297336070957223941543 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -25,6 +25,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do expect(response).to have_http_status(200) expect(json_response).to be_an Array + expect(response).to include_pagination_headers expect(json_response.count).to eq(1) expect(json_response.first['url']).to eq("http://example.com") expect(json_response.first['issues_events']).to eq(true) diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index eea76c7bb942c844bd7c9b5be9f723a6bcd78a82..f56876bcf54223976f6c9944d5be7f387fb5e3dd 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -16,9 +16,11 @@ describe API::ProjectSnippets, api: true do internal_snippet = create(:project_snippet, :internal, project: project) private_snippet = create(:project_snippet, :private, project: project) - get api("/projects/#{project.id}/snippets/", user) + get api("/projects/#{project.id}/snippets", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(3) expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id) expect(json_response.last).to have_key('web_url') @@ -28,7 +30,10 @@ describe API::ProjectSnippets, api: true do create(:project_snippet, :private, project: project) get api("/projects/#{project.id}/snippets/", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(0) end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 741815a780e105c456a8f6e4757f2c7765bfb15b..4e90aae927955c6f840dc0981938a2ee9efa7436 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -76,6 +76,7 @@ describe API::Projects, api: true do get api('/projects', user) expect(response.status).to eq 200 + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first.keys).to include('tag_list') end @@ -84,6 +85,7 @@ describe API::Projects, api: true do get api('/projects', user) expect(response.status).to eq 200 + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first.keys).to include('open_issues_count') end @@ -94,6 +96,7 @@ describe API::Projects, api: true do get api('/projects', user) expect(response.status).to eq 200 + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.find { |hash| hash['id'] == project.id }.keys).not_to include('open_issues_count') end @@ -102,6 +105,7 @@ describe API::Projects, api: true do get api('/projects', user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first).not_to include('statistics') end @@ -110,6 +114,7 @@ describe API::Projects, api: true do get api('/projects', user), statistics: true expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first).to include 'statistics' end @@ -121,6 +126,7 @@ describe API::Projects, api: true do get api('/projects?simple=true', user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first.keys).to match_array expected_keys end @@ -131,6 +137,7 @@ describe API::Projects, api: true do get api('/projects', user), { search: project.name } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) end @@ -141,6 +148,7 @@ describe API::Projects, api: true do get api('/projects', user), { visibility: 'private' } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.map { |p| p['id'] }).to contain_exactly(project.id, project2.id, project3.id) end @@ -151,6 +159,7 @@ describe API::Projects, api: true do get api('/projects', user), { visibility: 'internal' } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.map { |p| p['id'] }).to contain_exactly(project2.id) end @@ -159,6 +168,7 @@ describe API::Projects, api: true do get api('/projects', user), { visibility: 'public' } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.map { |p| p['id'] }).to contain_exactly(public_project.id) end @@ -169,6 +179,7 @@ describe API::Projects, api: true do get api('/projects', user), { order_by: 'id', sort: 'desc' } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['id']).to eq(project3.id) end @@ -179,6 +190,7 @@ describe API::Projects, api: true do get api('/projects', user4), owned: true expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['name']).to eq(project4.name) expect(json_response.first['owner']['username']).to eq(user4.username) @@ -197,6 +209,7 @@ describe API::Projects, api: true do get api('/projects', user3), starred: true expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id) end @@ -223,6 +236,7 @@ describe API::Projects, api: true do get api('/projects', user), { visibility: 'public', owned: true, starred: true, search: 'gitlab' } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) expect(json_response.first['id']).to eq(project5.id) @@ -644,9 +658,10 @@ describe API::Projects, api: true do get api("/projects/#{project.id}/events", current_user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array first_event = json_response.first - expect(first_event['action_name']).to eq('commented on') expect(first_event['note']['body']).to eq('What an awesome day!') @@ -699,11 +714,11 @@ describe API::Projects, api: true do get api("/projects/#{project.id}/users", current_user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) first_user = json_response.first - expect(first_user['username']).to eq(member.username) expect(first_user['name']).to eq(member.name) expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url]) @@ -746,7 +761,9 @@ describe API::Projects, api: true do it 'returns an array of project snippets' do get api("/projects/#{project.id}/snippets", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['title']).to eq(snippet.title) end @@ -1218,7 +1235,7 @@ describe API::Projects, api: true do end end - describe 'DELETE /projects/:id/star' do + describe 'POST /projects/:id/unstar' do context 'on a starred project' do before do user.toggle_star(project) @@ -1226,16 +1243,16 @@ describe API::Projects, api: true do end it 'unstars the project' do - expect { delete api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1) + expect { post api("/projects/#{project.id}/unstar", user) }.to change { project.reload.star_count }.by(-1) - expect(response).to have_http_status(200) + expect(response).to have_http_status(201) expect(json_response['star_count']).to eq(0) end end context 'on an unstarred project' do it 'does not modify the star count' do - expect { delete api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } + expect { post api("/projects/#{project.id}/unstar", user) }.not_to change { project.reload.star_count } expect(response).to have_http_status(304) end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index c61208e395c4a0dbe38d3305b0af2c1a034d044c..7652606a4910cb514a5e2311202f23756cfaf732 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -19,10 +19,10 @@ describe API::Repositories, api: true do get api(route, current_user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array first_commit = json_response.first - - expect(json_response).to be_an Array expect(first_commit['name']).to eq('bar') expect(first_commit['type']).to eq('tree') expect(first_commit['mode']).to eq('040000') @@ -49,6 +49,7 @@ describe API::Repositories, api: true do expect(response.status).to eq(200) expect(json_response).to be_an Array + expect(response).to include_pagination_headers expect(json_response[4]['name']).to eq('html') expect(json_response[4]['path']).to eq('files/html') expect(json_response[4]['type']).to eq('tree') @@ -380,10 +381,10 @@ describe API::Repositories, api: true do get api(route, current_user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array first_contributor = json_response.first - expect(first_contributor['email']).to eq('tiagonbotelho@hotmail.com') expect(first_contributor['name']).to eq('tiagonbotelho') expect(first_contributor['commits']).to eq(1) diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index f2d81a28cb8799b3502338119a041f82dd882538..103d6755888b15c874b7a3cab4c94dfedd87f3ec 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -37,18 +37,20 @@ describe API::Runners, api: true do context 'authorized user' do it 'returns user available runners' do get api('/runners', user) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any?{ |r| r['is_shared'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(shared).to be_falsey end it 'filters runners by scope' do get api('/runners?scope=active', user) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any?{ |r| r['is_shared'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(shared).to be_falsey end @@ -73,9 +75,10 @@ describe API::Runners, api: true do context 'with admin privileges' do it 'returns all runners' do get api('/runners/all', admin) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any?{ |r| r['is_shared'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(shared).to be_truthy end @@ -91,9 +94,10 @@ describe API::Runners, api: true do it 'filters runners by scope' do get api('/runners/all?scope=specific', admin) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any?{ |r| r['is_shared'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(shared).to be_falsey end @@ -183,6 +187,7 @@ describe API::Runners, api: true do it 'updates runner' do description = shared_runner.description active = shared_runner.active + runner_queue_value = shared_runner.ensure_runner_queue_value update_runner(shared_runner.id, admin, description: "#{description}_updated", active: !active, @@ -197,18 +202,24 @@ describe API::Runners, api: true do expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql') expect(shared_runner.run_untagged?).to be(false) expect(shared_runner.locked?).to be(true) + expect(shared_runner.ensure_runner_queue_value) + .not_to eq(runner_queue_value) end end context 'when runner is not shared' do it 'updates runner' do description = specific_runner.description + runner_queue_value = specific_runner.ensure_runner_queue_value + update_runner(specific_runner.id, admin, description: 'test') specific_runner.reload expect(response).to have_http_status(200) expect(specific_runner.description).to eq('test') expect(specific_runner.description).not_to eq(description) + expect(specific_runner.ensure_runner_queue_value) + .not_to eq(runner_queue_value) end end @@ -335,9 +346,10 @@ describe API::Runners, api: true do context 'authorized user with master privileges' do it "returns project's runners" do get api("/projects/#{project.id}/runners", user) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any?{ |r| r['is_shared'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(shared).to be_truthy end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 6b9a739b4390b94703e744cd4ea608f9af07a171..1ef92930b3c8e02947ed7284ed11a7961c339380 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -13,6 +13,8 @@ describe API::Snippets, api: true do get api("/snippets/", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( public_snippet.id, internal_snippet.id, @@ -25,7 +27,10 @@ describe API::Snippets, api: true do create(:personal_snippet, :private) get api("/snippets/", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(0) end end @@ -43,6 +48,8 @@ describe API::Snippets, api: true do get api("/snippets/public", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( public_snippet.id, public_snippet_other.id) diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index b3e5afdadb1599c8d1eb8ea6ab18735fea3f8b74..b59da632c0037e27f898b8ec132e1e471cb4b81a 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -31,6 +31,7 @@ describe API::SystemHooks, api: true do get api("/hooks", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['url']).to eq(hook.url) expect(json_response.first['push_events']).to be true diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index 898d2b27e5c31322ef1212c5ce06ffc01e58cab6..8a4f078182f34ad295dc37ddb1956b983d9a362b 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -20,10 +20,9 @@ describe API::Tags, api: true do get api("/projects/#{project.id}/repository/tags", current_user) expect(response).to have_http_status(200) - - first_tag = json_response.first - - expect(first_tag['name']).to eq(tag_name) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(tag_name) end end @@ -43,7 +42,9 @@ describe API::Tags, api: true do context 'without releases' do it "returns an array of project tags" do get api("/projects/#{project.id}/repository/tags", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['name']).to eq(tag_name) end @@ -59,6 +60,7 @@ describe API::Tags, api: true do get api("/projects/#{project.id}/repository/tags", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['name']).to eq(tag_name) expect(json_response.first['message']).to eq('Version 1.1.0') diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb index c0a8c0832bbc1b9d889c29c817d8e4994a74dd6d..8506e8fccdebe590ddf6abfe6601e5eab62b389c 100644 --- a/spec/requests/api/templates_spec.rb +++ b/spec/requests/api/templates_spec.rb @@ -22,6 +22,7 @@ describe API::Templates, api: true do get api('/templates/gitignores') expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to be > 15 end @@ -32,6 +33,7 @@ describe API::Templates, api: true do get api('/templates/gitlab_ci_ymls') expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['name']).not_to be_nil end @@ -69,6 +71,7 @@ describe API::Templates, api: true do get api('/templates/licenses') expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(15) expect(json_response.map { |l| l['key'] }).to include('agpl-3.0') @@ -80,6 +83,7 @@ describe API::Templates, api: true do get api('/templates/licenses?popular=1') expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(3) expect(json_response.map { |l| l['key'] }).to include('apache-2.0') diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index 56dc017ce54ba8c9e17e39717e75024f2ac3fb43..2069d2a7c75a0393142e363b6692fa33fb04f148 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -33,6 +33,7 @@ describe API::Todos, api: true do get api('/todos', john_doe) expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response[0]['id']).to eq(pending_3.id) @@ -52,6 +53,7 @@ describe API::Todos, api: true do get api('/todos', john_doe), { author_id: author_2.id } expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) end @@ -64,6 +66,7 @@ describe API::Todos, api: true do get api('/todos', john_doe), { type: 'MergeRequest' } expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) end @@ -74,6 +77,7 @@ describe API::Todos, api: true do get api('/todos', john_doe), { state: 'done' } expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) end @@ -84,6 +88,7 @@ describe API::Todos, api: true do get api('/todos', john_doe), { project_id: project_2.id } expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) end @@ -94,6 +99,7 @@ describe API::Todos, api: true do get api('/todos', john_doe), { action: 'mentioned' } expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) end diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 84104aa66ee94cd1a38a5bbf6319283f4038342a..92dfc2aa277308ec87d470e4f842cc0f47ac0350 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -100,6 +100,7 @@ describe API::Triggers do get api("/projects/#{project.id}/triggers", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_a(Array) expect(json_response[0]).to have_key('token') end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 5958012672ed4db6a0951802bc45461fabfb5a78..603da9f49fca9fe1c4f81fb0ac73fb1baa7eb043 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -40,7 +40,9 @@ describe API::Users, api: true do it "returns an array of users" do get api("/users", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array username = user.username expect(json_response.detect do |user| @@ -55,13 +57,16 @@ describe API::Users, api: true do get api("/users?blocked=true", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response).to all(include('state' => /(blocked|ldap_blocked)/)) end it "returns one user" do get api("/users?username=#{omniauth_user.username}", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['username']).to eq(omniauth_user.username) end @@ -70,7 +75,9 @@ describe API::Users, api: true do context "when admin" do it "returns an array of users" do get api("/users", admin) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first.keys).to include 'email' expect(json_response.first.keys).to include 'organization' @@ -87,6 +94,7 @@ describe API::Users, api: true do get api("/users?external=true", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response).to all(include('external' => true)) end @@ -507,8 +515,11 @@ describe API::Users, api: true do it 'returns array of ssh keys' do user.keys << key user.save + get api("/users/#{user.id}/keys", admin) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['title']).to eq(key.title) end @@ -595,8 +606,11 @@ describe API::Users, api: true do it 'returns array of emails' do user.emails << email user.save + get api("/users/#{user.id}/emails", admin) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['email']).to eq(email.email) end @@ -774,8 +788,11 @@ describe API::Users, api: true do it "returns array of ssh keys" do user.keys << key user.save + get api("/user/keys", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first["title"]).to eq(key.title) end @@ -891,8 +908,11 @@ describe API::Users, api: true do it "returns array of emails" do user.emails << email user.save + get api("/user/emails", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first["email"]).to eq(email.email) end @@ -983,69 +1003,69 @@ describe API::Users, api: true do end end - describe 'PUT /users/:id/block' do + describe 'POST /users/:id/block' do before { admin } it 'blocks existing user' do - put api("/users/#{user.id}/block", admin) - expect(response).to have_http_status(200) + post api("/users/#{user.id}/block", admin) + expect(response).to have_http_status(201) expect(user.reload.state).to eq('blocked') end it 'does not re-block ldap blocked users' do - put api("/users/#{ldap_blocked_user.id}/block", admin) + post api("/users/#{ldap_blocked_user.id}/block", admin) expect(response).to have_http_status(403) expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') end it 'does not be available for non admin users' do - put api("/users/#{user.id}/block", user) + post api("/users/#{user.id}/block", user) expect(response).to have_http_status(403) expect(user.reload.state).to eq('active') end it 'returns a 404 error if user id not found' do - put api('/users/9999/block', admin) + post api('/users/9999/block', admin) expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end end - describe 'PUT /users/:id/unblock' do + describe 'POST /users/:id/unblock' do let(:blocked_user) { create(:user, state: 'blocked') } before { admin } it 'unblocks existing user' do - put api("/users/#{user.id}/unblock", admin) - expect(response).to have_http_status(200) + post api("/users/#{user.id}/unblock", admin) + expect(response).to have_http_status(201) expect(user.reload.state).to eq('active') end it 'unblocks a blocked user' do - put api("/users/#{blocked_user.id}/unblock", admin) - expect(response).to have_http_status(200) + post api("/users/#{blocked_user.id}/unblock", admin) + expect(response).to have_http_status(201) expect(blocked_user.reload.state).to eq('active') end it 'does not unblock ldap blocked users' do - put api("/users/#{ldap_blocked_user.id}/unblock", admin) + post api("/users/#{ldap_blocked_user.id}/unblock", admin) expect(response).to have_http_status(403) expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') end it 'does not be available for non admin users' do - put api("/users/#{user.id}/unblock", user) + post api("/users/#{user.id}/unblock", user) expect(response).to have_http_status(403) expect(user.reload.state).to eq('active') end it 'returns a 404 error if user id not found' do - put api('/users/9999/block', admin) + post api('/users/9999/block', admin) expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end it "returns a 404 for invalid ID" do - put api("/users/ASDF/block", admin) + post api("/users/ASDF/block", admin) expect(response).to have_http_status(404) end @@ -1073,14 +1093,14 @@ describe API::Users, api: true do end context "as a user than can see the event's project" do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/users/#{user.id}/events", user) } - end - context 'joined event' do it 'returns the "joined" event' do get api("/users/#{user.id}/events", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + comment_event = json_response.find { |e| e['action_name'] == 'commented on' } expect(comment_event['project_id'].to_i).to eq(project.id) diff --git a/spec/requests/api/v3/boards_spec.rb b/spec/requests/api/v3/boards_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..8aaf3be4f876553ba9c65312ee5bd583dce25f3a --- /dev/null +++ b/spec/requests/api/v3/boards_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe API::V3::Boards, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:guest) { create(:user) } + let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) } + + let!(:dev_label) do + create(:label, title: 'Development', color: '#FFAABB', project: project) + end + + let!(:test_label) do + create(:label, title: 'Testing', color: '#FFAACC', project: project) + end + + let!(:dev_list) do + create(:list, label: dev_label, position: 1) + end + + let!(:test_list) do + create(:list, label: test_label, position: 2) + end + + let!(:board) do + create(:board, project: project, lists: [dev_list, test_list]) + end + + before do + project.team << [user, :reporter] + project.team << [guest, :guest] + end + + describe "GET /projects/:id/boards" do + let(:base_url) { "/projects/#{project.id}/boards" } + + context "when unauthenticated" do + it "returns authentication error" do + get v3_api(base_url) + + expect(response).to have_http_status(401) + end + end + + context "when authenticated" do + it "returns the project issue board" do + get v3_api(base_url, user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(board.id) + expect(json_response.first['lists']).to be_an Array + expect(json_response.first['lists'].length).to eq(2) + expect(json_response.first['lists'].last).to have_key('position') + end + end + end + + describe "GET /projects/:id/boards/:board_id/lists" do + let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } + + it 'returns issue board lists' do + get v3_api(base_url, user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['label']['name']).to eq(dev_label.title) + end + + it 'returns 404 if board not found' do + get v3_api("/projects/#{project.id}/boards/22343/lists", user) + + expect(response).to have_http_status(404) + end + end +end diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..0e4c6bc3bc63c6eebbabb49a221b84c2081978c5 --- /dev/null +++ b/spec/requests/api/v3/branches_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' +require 'mime/types' + +describe API::V3::Branches, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let!(:project) { create(:project, :repository, creator: user) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + + describe "GET /projects/:id/repository/branches" do + it "returns an array of project branches" do + project.repository.expire_all_method_caches + + get v3_api("/projects/#{project.id}/repository/branches", user), per_page: 100 + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + branch_names = json_response.map { |x| x['name'] } + expect(branch_names).to match_array(project.repository.branch_names) + end + end +end diff --git a/spec/requests/api/v3/labels_spec.rb b/spec/requests/api/v3/labels_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..18e2c0d40c8b1c7a121ef1e76931c316f28b4ed6 --- /dev/null +++ b/spec/requests/api/v3/labels_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +describe API::V3::Labels, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) } + let!(:label1) { create(:label, title: 'label1', project: project) } + let!(:priority_label) { create(:label, title: 'bug', project: project, priority: 3) } + + before do + project.team << [user, :master] + end + + describe 'GET /projects/:id/labels' do + it 'returns all available labels to the project' do + group = create(:group) + group_label = create(:group_label, title: 'feature', group: group) + project.update(group: group) + create(:labeled_issue, project: project, labels: [group_label], author: user) + create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed) + create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project ) + + expected_keys = [ + 'id', 'name', 'color', 'description', + 'open_issues_count', 'closed_issues_count', 'open_merge_requests_count', + 'subscribed', 'priority' + ] + + get v3_api("/projects/#{project.id}/labels", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(3) + expect(json_response.first.keys).to match_array expected_keys + expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, priority_label.name, label1.name]) + + label1_response = json_response.find { |l| l['name'] == label1.title } + group_label_response = json_response.find { |l| l['name'] == group_label.title } + priority_label_response = json_response.find { |l| l['name'] == priority_label.title } + + expect(label1_response['open_issues_count']).to eq(0) + expect(label1_response['closed_issues_count']).to eq(1) + expect(label1_response['open_merge_requests_count']).to eq(0) + expect(label1_response['name']).to eq(label1.name) + expect(label1_response['color']).to be_present + expect(label1_response['description']).to be_nil + expect(label1_response['priority']).to be_nil + expect(label1_response['subscribed']).to be_falsey + + expect(group_label_response['open_issues_count']).to eq(1) + expect(group_label_response['closed_issues_count']).to eq(0) + expect(group_label_response['open_merge_requests_count']).to eq(0) + expect(group_label_response['name']).to eq(group_label.name) + expect(group_label_response['color']).to be_present + expect(group_label_response['description']).to be_nil + expect(group_label_response['priority']).to be_nil + expect(group_label_response['subscribed']).to be_falsey + + expect(priority_label_response['open_issues_count']).to eq(0) + expect(priority_label_response['closed_issues_count']).to eq(0) + expect(priority_label_response['open_merge_requests_count']).to eq(1) + expect(priority_label_response['name']).to eq(priority_label.name) + expect(priority_label_response['color']).to be_present + expect(priority_label_response['description']).to be_nil + expect(priority_label_response['priority']).to eq(3) + expect(priority_label_response['subscribed']).to be_falsey + end + end +end diff --git a/spec/requests/api/v3/repositories_spec.rb b/spec/requests/api/v3/repositories_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c696721c1c9177e8f6919606f03f246a5955219b --- /dev/null +++ b/spec/requests/api/v3/repositories_spec.rb @@ -0,0 +1,144 @@ +require 'spec_helper' +require 'mime/types' + +describe API::V3::Repositories, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } } + let!(:project) { create(:project, :repository, creator: user) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + + describe "GET /projects/:id/repository/tree" do + let(:route) { "/projects/#{project.id}/repository/tree" } + + shared_examples_for 'repository tree' do + it 'returns the repository tree' do + get v3_api(route, current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + + first_commit = json_response.first + expect(first_commit['name']).to eq('bar') + expect(first_commit['type']).to eq('tree') + expect(first_commit['mode']).to eq('040000') + end + + context 'when ref does not exist' do + it_behaves_like '404 response' do + let(:request) { get v3_api("#{route}?ref_name=foo", current_user) } + let(:message) { '404 Tree Not Found' } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get v3_api(route, current_user) } + end + end + + context 'with recursive=1' do + it 'returns recursive project paths tree' do + get v3_api("#{route}?recursive=1", current_user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response[4]['name']).to eq('html') + expect(json_response[4]['path']).to eq('files/html') + expect(json_response[4]['type']).to eq('tree') + expect(json_response[4]['mode']).to eq('040000') + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get v3_api(route, current_user) } + end + end + + context 'when ref does not exist' do + it_behaves_like '404 response' do + let(:request) { get v3_api("#{route}?recursive=1&ref_name=foo", current_user) } + let(:message) { '404 Tree Not Found' } + end + end + end + end + + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository tree' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end + end + + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get v3_api(route) } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository tree' do + let(:current_user) { user } + end + end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get v3_api(route, guest) } + end + end + end + + describe 'GET /projects/:id/repository/contributors' do + let(:route) { "/projects/#{project.id}/repository/contributors" } + + shared_examples_for 'repository contributors' do + it 'returns valid data' do + get v3_api(route, current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + + first_contributor = json_response.first + expect(first_contributor['email']).to eq('tiagonbotelho@hotmail.com') + expect(first_contributor['name']).to eq('tiagonbotelho') + expect(first_contributor['commits']).to eq(1) + expect(first_contributor['additions']).to eq(0) + expect(first_contributor['deletions']).to eq(0) + end + end + + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository contributors' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end + end + + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get v3_api(route) } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository contributors' do + let(:current_user) { user } + end + end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get v3_api(route, guest) } + end + end + end +end diff --git a/spec/requests/api/v3/system_hooks_spec.rb b/spec/requests/api/v3/system_hooks_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..da58efb6ebf0de1a9fcab1dc89c98e4d0a26cc49 --- /dev/null +++ b/spec/requests/api/v3/system_hooks_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe API::V3::SystemHooks, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + let!(:hook) { create(:system_hook, url: "http://example.com") } + + before { stub_request(:post, hook.url) } + + describe "GET /hooks" do + context "when no user" do + it "returns authentication error" do + get v3_api("/hooks") + + expect(response).to have_http_status(401) + end + end + + context "when not an admin" do + it "returns forbidden error" do + get v3_api("/hooks", user) + + expect(response).to have_http_status(403) + end + end + + context "when authenticated as admin" do + it "returns an array of hooks" do + get v3_api("/hooks", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['url']).to eq(hook.url) + expect(json_response.first['push_events']).to be true + expect(json_response.first['tag_push_events']).to be false + end + end + end +end diff --git a/spec/requests/api/v3/tags_spec.rb b/spec/requests/api/v3/tags_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..6722789d928aac30d97f6be3adf4dfc432eeab08 --- /dev/null +++ b/spec/requests/api/v3/tags_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' +require 'mime/types' + +describe API::V3::Tags, api: true do + include ApiHelpers + include RepoHelpers + + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:project) { create(:project, :repository, creator: user) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + + describe "GET /projects/:id/repository/tags" do + let(:tag_name) { project.repository.tag_names.sort.reverse.first } + let(:description) { 'Awesome release!' } + + shared_examples_for 'repository tags' do + it 'returns the repository tags' do + get v3_api("/projects/#{project.id}/repository/tags", current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(tag_name) + end + end + + context 'when unauthenticated' do + it_behaves_like 'repository tags' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end + end + + context 'when authenticated' do + it_behaves_like 'repository tags' do + let(:current_user) { user } + end + end + + context 'without releases' do + it "returns an array of project tags" do + get v3_api("/projects/#{project.id}/repository/tags", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(tag_name) + end + end + + context 'with releases' do + before do + release = project.releases.find_or_initialize_by(tag: tag_name) + release.update_attributes(description: description) + end + + it "returns an array of project tags with release info" do + get v3_api("/projects/#{project.id}/repository/tags", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(tag_name) + expect(json_response.first['message']).to eq('Version 1.1.0') + expect(json_response.first['release']['description']).to eq(description) + end + end + end +end diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5020ef18a3a8f7d568a962edd3f7fe2315a3a8a9 --- /dev/null +++ b/spec/requests/api/v3/users_spec.rb @@ -0,0 +1,189 @@ +require 'spec_helper' + +describe API::V3::Users, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + let(:key) { create(:key, user: user) } + let(:email) { create(:email, user: user) } + let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } + + describe 'GET /user/:id/keys' do + before { admin } + + context 'when unauthenticated' do + it 'returns authentication error' do + get v3_api("/users/#{user.id}/keys") + expect(response).to have_http_status(401) + end + end + + context 'when authenticated' do + it 'returns 404 for non-existing user' do + get v3_api('/users/999999/keys', admin) + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns array of ssh keys' do + user.keys << key + user.save + + get v3_api("/users/#{user.id}/keys", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(key.title) + end + end + end + + describe 'GET /user/:id/emails' do + before { admin } + + context 'when unauthenticated' do + it 'returns authentication error' do + get v3_api("/users/#{user.id}/emails") + expect(response).to have_http_status(401) + end + end + + context 'when authenticated' do + it 'returns 404 for non-existing user' do + get v3_api('/users/999999/emails', admin) + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns array of emails' do + user.emails << email + user.save + + get v3_api("/users/#{user.id}/emails", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['email']).to eq(email.email) + end + + it "returns a 404 for invalid ID" do + put v3_api("/users/ASDF/emails", admin) + + expect(response).to have_http_status(404) + end + end + end + + describe "GET /user/keys" do + context "when unauthenticated" do + it "returns authentication error" do + get v3_api("/user/keys") + expect(response).to have_http_status(401) + end + end + + context "when authenticated" do + it "returns array of ssh keys" do + user.keys << key + user.save + + get v3_api("/user/keys", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first["title"]).to eq(key.title) + end + end + end + + describe "GET /user/emails" do + context "when unauthenticated" do + it "returns authentication error" do + get v3_api("/user/emails") + expect(response).to have_http_status(401) + end + end + + context "when authenticated" do + it "returns array of emails" do + user.emails << email + user.save + + get v3_api("/user/emails", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first["email"]).to eq(email.email) + end + end + end + + describe 'PUT /users/:id/block' do + before { admin } + it 'blocks existing user' do + put v3_api("/users/#{user.id}/block", admin) + expect(response).to have_http_status(200) + expect(user.reload.state).to eq('blocked') + end + + it 'does not re-block ldap blocked users' do + put v3_api("/users/#{ldap_blocked_user.id}/block", admin) + expect(response).to have_http_status(403) + expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') + end + + it 'does not be available for non admin users' do + put v3_api("/users/#{user.id}/block", user) + expect(response).to have_http_status(403) + expect(user.reload.state).to eq('active') + end + + it 'returns a 404 error if user id not found' do + put v3_api('/users/9999/block', admin) + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + end + + describe 'PUT /users/:id/unblock' do + let(:blocked_user) { create(:user, state: 'blocked') } + before { admin } + + it 'unblocks existing user' do + put v3_api("/users/#{user.id}/unblock", admin) + expect(response).to have_http_status(200) + expect(user.reload.state).to eq('active') + end + + it 'unblocks a blocked user' do + put v3_api("/users/#{blocked_user.id}/unblock", admin) + expect(response).to have_http_status(200) + expect(blocked_user.reload.state).to eq('active') + end + + it 'does not unblock ldap blocked users' do + put v3_api("/users/#{ldap_blocked_user.id}/unblock", admin) + expect(response).to have_http_status(403) + expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') + end + + it 'does not be available for non admin users' do + put v3_api("/users/#{user.id}/unblock", user) + expect(response).to have_http_status(403) + expect(user.reload.state).to eq('active') + end + + it 'returns a 404 error if user id not found' do + put v3_api('/users/9999/block', admin) + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it "returns a 404 for invalid ID" do + put v3_api("/users/ASDF/block", admin) + + expect(response).to have_http_status(404) + end + end +end diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb index 1b95f1ff1982a144f0a51d7f27b565e637bc10ea..6a6df377b35337fe8a45c353fcaaf2e0a08a30b6 100644 --- a/spec/serializers/environment_serializer_spec.rb +++ b/spec/serializers/environment_serializer_spec.rb @@ -181,6 +181,17 @@ describe EnvironmentSerializer do expect(subject.first[:name]).to eq 'production' expect(subject.second[:name]).to eq 'staging' end + + it 'appends correct total page count header' do + expect(subject).not_to be_empty + expect(response).to have_received(:[]=).with('X-Total', '3') + end + + it 'appends correct page count headers' do + expect(subject).not_to be_empty + expect(response).to have_received(:[]=).with('X-Total-Pages', '2') + expect(response).to have_received(:[]=).with('X-Per-Page', '2') + end end end end diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index ebb11166964a8f1f1b0e55c4d176a22ddcf1efa7..ef2ddc4b1d782391542eb16551b6baecbe9df2f9 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -1,8 +1,16 @@ require 'spec_helper' -describe Ci::ProcessPipelineService, services: true do - let(:pipeline) { create(:ci_empty_pipeline, ref: 'master') } +describe Ci::ProcessPipelineService, :services do let(:user) { create(:user) } + let(:project) { create(:empty_project) } + + let(:pipeline) do + create(:ci_empty_pipeline, ref: 'master', project: project) + end + + before do + project.add_developer(user) + end describe '#execute' do context 'start queuing next builds' do @@ -285,7 +293,7 @@ describe Ci::ProcessPipelineService, services: true do expect(builds.pluck(:name)) .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2') - Ci::Build.retry(pipeline.builds.find_by(name: 'test:2')).success + Ci::Build.retry(pipeline.builds.find_by(name: 'test:2'), user).success expect(builds.pluck(:name)).to contain_exactly( 'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2') diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..93147870afeb10a2e0ab517d64f417a68a64dcc8 --- /dev/null +++ b/spec/services/ci/retry_build_service_spec.rb @@ -0,0 +1,117 @@ +require 'spec_helper' + +describe Ci::RetryBuildService, :services do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } + + let(:service) do + described_class.new(project, user) + end + + shared_examples 'build duplication' do + let(:build) do + create(:ci_build, :failed, :artifacts, :erased, :trace, + :queued, :coverage, pipeline: pipeline) + end + + describe 'clone attributes' do + described_class::CLONE_ATTRIBUTES.each do |attribute| + it "clones #{attribute} build attribute" do + expect(new_build.send(attribute)).to eq build.send(attribute) + end + end + end + + describe 'reject attributes' do + described_class::REJECT_ATTRIBUTES.each do |attribute| + it "does not clone #{attribute} build attribute" do + expect(new_build.send(attribute)).not_to eq build.send(attribute) + end + end + end + + it 'has correct number of known attributes' do + attributes = + described_class::CLONE_ATTRIBUTES + + described_class::IGNORE_ATTRIBUTES + + described_class::REJECT_ATTRIBUTES + + expect(attributes.size).to eq build.attributes.size + end + end + + describe '#execute' do + let(:new_build) { service.execute(build) } + + context 'when user has ability to execute build' do + before do + project.add_developer(user) + end + + it_behaves_like 'build duplication' + + it 'creates a new build that represents the old one' do + expect(new_build.name).to eq build.name + end + + it 'enqueues the new build' do + expect(new_build).to be_pending + end + + it 'resolves todos for old build that failed' do + expect(MergeRequests::AddTodoWhenBuildFailsService) + .to receive_message_chain(:new, :close) + + service.execute(build) + end + + context 'when there are subsequent builds that are skipped' do + let!(:subsequent_build) do + create(:ci_build, :skipped, stage_idx: 1, pipeline: pipeline) + end + + it 'resumes pipeline processing in subsequent stages' do + service.execute(build) + + expect(subsequent_build.reload).to be_created + end + end + end + + context 'when user does not have ability to execute build' do + it 'raises an error' do + expect { service.execute(build) } + .to raise_error Gitlab::Access::AccessDeniedError + end + end + end + + describe '#reprocess' do + let(:new_build) { service.reprocess(build) } + + context 'when user has ability to execute build' do + before do + project.add_developer(user) + end + + it_behaves_like 'build duplication' + + it 'creates a new build that represents the old one' do + expect(new_build.name).to eq build.name + end + + it 'does not enqueue the new build' do + expect(new_build).to be_created + end + end + + context 'when user does not have ability to execute build' do + it 'raises an error' do + expect { service.reprocess(build) } + .to raise_error Gitlab::Access::AccessDeniedError + end + end + end +end diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c0af8b8450a4a02fbb03a7a4a4dbcd9cd61bc6db --- /dev/null +++ b/spec/services/ci/retry_pipeline_service_spec.rb @@ -0,0 +1,175 @@ +require 'spec_helper' + +describe Ci::RetryPipelineService, '#execute', :services do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:service) { described_class.new(project, user) } + + context 'when user has ability to modify pipeline' do + let(:user) { create(:admin) } + + context 'when there are failed builds in the last stage' do + before do + create_build('rspec 1', :success, 0) + create_build('rspec 2', :failed, 1) + create_build('rspec 3', :canceled, 1) + end + + it 'enqueues all builds in the last stage' do + service.execute(pipeline) + + expect(build('rspec 2')).to be_pending + expect(build('rspec 3')).to be_pending + expect(pipeline.reload).to be_running + end + end + + context 'when there are failed or canceled builds in the first stage' do + before do + create_build('rspec 1', :failed, 0) + create_build('rspec 2', :canceled, 0) + create_build('rspec 3', :canceled, 1) + create_build('spinach 1', :canceled, 2) + end + + it 'retries builds failed builds and marks subsequent for processing' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('rspec 2')).to be_pending + expect(build('rspec 3')).to be_created + expect(build('spinach 1')).to be_created + expect(pipeline.reload).to be_running + end + end + + context 'when there is failed build present which was run on failure' do + before do + create_build('rspec 1', :failed, 0) + create_build('rspec 2', :canceled, 0) + create_build('rspec 3', :canceled, 1) + create_build('report 1', :failed, 2) + end + + it 'retries builds only in the first stage' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('rspec 2')).to be_pending + expect(build('rspec 3')).to be_created + expect(build('report 1')).to be_created + expect(pipeline.reload).to be_running + end + + it 'creates a new job for report job in this case' do + service.execute(pipeline) + + expect(statuses.where(name: 'report 1').first).to be_retried + end + end + + context 'when pipeline contains manual actions' do + context 'when there is a canceled manual action in first stage' do + before do + create_build('rspec 1', :failed, 0) + create_build('staging', :canceled, 0, :manual) + create_build('rspec 2', :canceled, 1) + end + + it 'retries builds failed builds and marks subsequent for processing' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('staging')).to be_skipped + expect(build('rspec 2')).to be_created + expect(pipeline.reload).to be_running + end + end + + context 'when there is a skipped manual action in last stage' do + before do + create_build('rspec 1', :canceled, 0) + create_build('staging', :skipped, 1, :manual) + end + + it 'retries canceled job and skips manual action' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('staging')).to be_skipped + expect(pipeline.reload).to be_running + end + end + + context 'when there is a created manual action in the last stage' do + before do + create_build('rspec 1', :canceled, 0) + create_build('staging', :created, 1, :manual) + end + + it 'retries canceled job and does not update the manual action' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('staging')).to be_created + expect(pipeline.reload).to be_running + end + end + + context 'when there is a created manual action in the first stage' do + before do + create_build('rspec 1', :canceled, 0) + create_build('staging', :created, 0, :manual) + end + + it 'retries canceled job and skipps the manual action' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('staging')).to be_skipped + expect(pipeline.reload).to be_running + end + end + end + + it 'closes all todos about failed jobs for pipeline' do + expect(MergeRequests::AddTodoWhenBuildFailsService) + .to receive_message_chain(:new, :close_all) + + service.execute(pipeline) + end + + it 'reprocesses the pipeline' do + expect(pipeline).to receive(:process!) + + service.execute(pipeline) + end + end + + context 'when user is not allowed to retry pipeline' do + it 'raises an error' do + expect { service.execute(pipeline) } + .to raise_error Gitlab::Access::AccessDeniedError + end + end + + def statuses + pipeline.reload.statuses + end + + def build(name) + statuses.latest.find_by(name: name) + end + + def create_build(name, status, stage_num, on = 'on_success') + create(:ci_build, name: name, + status: status, + stage: "stage_#{stage_num}", + stage_idx: stage_num, + when: on, + pipeline: pipeline) do |build| + pipeline.update_status + end + end +end diff --git a/spec/services/ci/update_runner_service_spec.rb b/spec/services/ci/update_runner_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e429fcfc72fc95c8bf4a688d3f368dedf1fc7ad3 --- /dev/null +++ b/spec/services/ci/update_runner_service_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Ci::UpdateRunnerService, :services do + let(:runner) { create(:ci_runner) } + + describe '#update' do + before do + allow(runner).to receive(:tick_runner_queue) + end + + context 'with description params' do + let(:params) { { description: 'new runner' } } + + it 'updates the runner and ticking the queue' do + expect(update).to be_truthy + + runner.reload + + expect(runner).to have_received(:tick_runner_queue) + expect(runner.description).to eq('new runner') + end + end + + context 'when params are not valid' do + let(:params) { { run_untagged: false } } + + it 'does not update and give false because it is not valid' do + expect(update).to be_falsey + + runner.reload + + expect(runner).not_to have_received(:tick_runner_queue) + expect(runner.run_untagged).to be_truthy + end + end + + def update + described_class.new(runner).update(params) + end + end +end diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index cf0a18aacec705ceeae36eab5920242e770e88bf..6fb4d517115f0bc6b793df52386e21c0edc06a5c 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -234,7 +234,11 @@ describe CreateDeploymentService, services: true do context 'when build is retried' do it_behaves_like 'does create environment and deployment' do - let(:deployable) { Ci::Build.retry(build) } + before do + project.add_developer(user) + end + + let(:deployable) { Ci::Build.retry(build, user) } subject { deployable.success } end diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index f86189b68e9ab5f8b66b74a341db1535a7870c4b..32c2ed8cae7cccaced9a96d1c6cf5ca5851f5390 100644 --- a/spec/services/groups/destroy_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -9,14 +9,18 @@ describe Groups::DestroyService, services: true do let!(:gitlab_shell) { Gitlab::Shell.new } let!(:remove_path) { group.path + "+#{group.id}+deleted" } + before do + group.add_user(user, Gitlab::Access::OWNER) + end + shared_examples 'group destruction' do |async| context 'database records' do before do destroy_group(group, user, async) end - it { expect(Group.all).not_to include(group) } - it { expect(Project.all).not_to include(project) } + it { expect(Group.unscoped.all).not_to include(group) } + it { expect(Project.unscoped.all).not_to include(project) } end context 'file system' do @@ -32,7 +36,7 @@ describe Groups::DestroyService, services: true do context 'Sidekiq fake' do before do - # Dont run sidekiq to check if renamed repository exists + # Don't run sidekiq to check if renamed repository exists Sidekiq::Testing.fake! { destroy_group(group, user, async) } end @@ -95,4 +99,13 @@ describe Groups::DestroyService, services: true do describe 'synchronous delete' do it_behaves_like 'group destruction', false end + + context 'projects in pending_delete' do + before do + project.pending_delete = true + project.save + end + + it_behaves_like 'group destruction', false + end end diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb index bb7830c7eea782e79f356a08b6a9e1ab4b1a3135..d80fb8a1af19c107b923c05188f16f885c51db87 100644 --- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb +++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb @@ -17,7 +17,7 @@ describe MergeRequests::AddTodoWhenBuildFailsService do described_class.new(project, user, commit_message: 'Awesome message') end - let(:todo_service) { TodoService.new } + let(:todo_service) { spy('todo service') } let(:merge_request) do create(:merge_request, merge_user: user, @@ -107,4 +107,27 @@ describe MergeRequests::AddTodoWhenBuildFailsService do end end end + + describe '#close_all' do + context 'when using pipeline that belongs to merge request' do + it 'resolves todos about failed builds for pipeline' do + service.close_all(pipeline) + + expect(todo_service) + .to have_received(:merge_request_build_retried) + .with(merge_request) + end + end + + context 'when pipeline is not related to merge request' do + let(:pipeline) { create(:ci_empty_pipeline) } + + it 'does not resolve any todos about failed builds' do + service.close_all(pipeline) + + expect(todo_service) + .not_to have_received(:merge_request_build_retried) + end + end + end end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 3faa88c00a1753ada51b2d982bc57cc029d2c026..74bfba44dfd5ee15434495eea1437dafc6f37d8d 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -50,6 +50,25 @@ describe Projects::DestroyService, services: true do it { expect(Dir.exist?(remove_path)).to be_truthy } end + context 'when flushing caches fail' do + before do + new_user = create(:user) + project.team.add_user(new_user, Gitlab::Access::DEVELOPER) + allow_any_instance_of(Projects::DestroyService).to receive(:flush_caches).and_raise(Redis::CannotConnectError) + end + + it 'keeps project team intact upon an error' do + Sidekiq::Testing.inline! do + begin + destroy_project(project, user, {}) + rescue Redis::CannotConnectError + end + end + + expect(project.team.members.count).to eq 1 + end + end + context 'with async_execute' do let(:async) { true } diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 4320365ab57b94c2266519956dbe0818ea324a5f..9f24cc0f3f230fde10873c1d41cd8108b536bcde 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -287,39 +287,51 @@ describe TodoService, services: true do end end - shared_examples 'marking todos as done' do |meth| - let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } - let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } + shared_examples 'updating todos state' do |meth, state, new_state| + let!(:first_todo) { create(:todo, state, user: john_doe, project: project, target: issue, author: author) } + let!(:second_todo) { create(:todo, state, user: john_doe, project: project, target: issue, author: author) } - it 'marks related todos for the user as done' do + it 'updates related todos for the user with the new_state' do service.send(meth, collection, john_doe) - expect(first_todo.reload).to be_done - expect(second_todo.reload).to be_done + expect(first_todo.reload.state?(new_state)).to be true + expect(second_todo.reload.state?(new_state)).to be true end describe 'cached counts' do it 'updates when todos change' do - expect(john_doe.todos_done_count).to eq(0) - expect(john_doe.todos_pending_count).to eq(2) + expect(john_doe.todos.where(state: new_state).count).to eq(0) + expect(john_doe.todos.where(state: state).count).to eq(2) expect(john_doe).to receive(:update_todos_count_cache).and_call_original service.send(meth, collection, john_doe) - expect(john_doe.todos_done_count).to eq(2) - expect(john_doe.todos_pending_count).to eq(0) + expect(john_doe.todos.where(state: new_state).count).to eq(2) + expect(john_doe.todos.where(state: state).count).to eq(0) end end end describe '#mark_todos_as_done' do - it_behaves_like 'marking todos as done', :mark_todos_as_done do + it_behaves_like 'updating todos state', :mark_todos_as_done, :pending, :done do let(:collection) { [first_todo, second_todo] } end end describe '#mark_todos_as_done_by_ids' do - it_behaves_like 'marking todos as done', :mark_todos_as_done_by_ids do + it_behaves_like 'updating todos state', :mark_todos_as_done_by_ids, :pending, :done do + let(:collection) { [first_todo, second_todo].map(&:id) } + end + end + + describe '#mark_todos_as_pending' do + it_behaves_like 'updating todos state', :mark_todos_as_pending, :done, :pending do + let(:collection) { [first_todo, second_todo] } + end + end + + describe '#mark_todos_as_pending_by_ids' do + it_behaves_like 'updating todos state', :mark_todos_as_pending_by_ids, :done, :pending do let(:collection) { [first_todo, second_todo].map(&:id) } end end diff --git a/spec/support/api/pagination_shared_examples.rb b/spec/support/api/pagination_shared_examples.rb deleted file mode 100644 index 352a6eeec79fb7dcf84dc5bc8687529ff6026227..0000000000000000000000000000000000000000 --- a/spec/support/api/pagination_shared_examples.rb +++ /dev/null @@ -1,20 +0,0 @@ -# Specs for paginated resources. -# -# Requires an API request: -# let(:request) { get api("/projects/#{project.id}/repository/branches", user) } -shared_examples 'a paginated resources' do - before do - # Fires the request - request - end - - it 'has pagination headers' do - expect(response.headers).to include('X-Total') - expect(response.headers).to include('X-Total-Pages') - expect(response.headers).to include('X-Per-Page') - expect(response.headers).to include('X-Page') - expect(response.headers).to include('X-Next-Page') - expect(response.headers).to include('X-Prev-Page') - expect(response.headers).to include('Link') - end -end diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index 247f0954221cc34d03acf64a2b0526924988af70..6f31828b8254c32869f60c24047464532a002b48 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -3,6 +3,10 @@ RSpec.configure do |config| DatabaseCleaner.clean_with(:truncation) end + config.append_after(:context) do + DatabaseCleaner.clean_with(:truncation) + end + config.before(:each) do DatabaseCleaner.strategy = :transaction end diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json index ce8dfe5ae7559b755a4735f36c93b146cc8430bc..cd55d63125e5975316bf1504b1a9192d1c278039 100644 --- a/spec/support/gitlab_stubs/session.json +++ b/spec/support/gitlab_stubs/session.json @@ -7,7 +7,7 @@ "skype":"aertert", "linkedin":"", "twitter":"", - "theme_id":2,"color_scheme_id":2, + "color_scheme_id":2, "state":"active", "created_at":"2012-12-21T13:02:20Z", "extern_uid":null, @@ -17,4 +17,4 @@ "can_create_project":false, "private_token":"Wvjy2Krpb7y8xi93owUz", "access_token":"Wvjy2Krpb7y8xi93owUz" -} \ No newline at end of file +} diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json index ce8dfe5ae7559b755a4735f36c93b146cc8430bc..cd55d63125e5975316bf1504b1a9192d1c278039 100644 --- a/spec/support/gitlab_stubs/user.json +++ b/spec/support/gitlab_stubs/user.json @@ -7,7 +7,7 @@ "skype":"aertert", "linkedin":"", "twitter":"", - "theme_id":2,"color_scheme_id":2, + "color_scheme_id":2, "state":"active", "created_at":"2012-12-21T13:02:20Z", "extern_uid":null, @@ -17,4 +17,4 @@ "can_create_project":false, "private_token":"Wvjy2Krpb7y8xi93owUz", "access_token":"Wvjy2Krpb7y8xi93owUz" -} \ No newline at end of file +} diff --git a/spec/support/matchers/pagination_matcher.rb b/spec/support/matchers/pagination_matcher.rb new file mode 100644 index 0000000000000000000000000000000000000000..60f5e8239a735fc0a372ea7942b03152e5c75a97 --- /dev/null +++ b/spec/support/matchers/pagination_matcher.rb @@ -0,0 +1,5 @@ +RSpec::Matchers.define :include_pagination_headers do |expected| + match do |actual| + expect(actual.headers).to include('X-Total', 'X-Total-Pages', 'X-Per-Page', 'X-Page', 'X-Next-Page', 'X-Prev-Page', 'Link') + end +end diff --git a/yarn.lock b/yarn.lock index 99db6f61bcd9734a44d79012a8d04a6e5ba6f0b0..ad4b5223d60dfe964ede15c55c26a0d26729845d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -852,7 +852,7 @@ boom@2.x.x: dependencies: hoek "2.x.x" -bootstrap-sass@3.3.6: +bootstrap-sass@^3.3.6: version "3.3.6" resolved "https://registry.yarnpkg.com/bootstrap-sass/-/bootstrap-sass-3.3.6.tgz#363b0d300e868d3e70134c1a742bb17288444fd1" @@ -1265,7 +1265,7 @@ custom-event@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" -d3@3.5.11: +d3@^3.5.11: version "3.5.11" resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.11.tgz#d130750eed0554db70e8432102f920a12407b69c" @@ -1404,7 +1404,7 @@ domain-browser@^1.1.1: version "1.1.7" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" -dropzone@4.2.0: +dropzone@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/dropzone/-/dropzone-4.2.0.tgz#fbe7acbb9918e0706489072ef663effeef8a79f3" @@ -2273,13 +2273,6 @@ ignore@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.2.tgz#1c51e1ef53bab6ddc15db4d9ac4ec139eceb3410" -imports-loader@^0.6.5: - version "0.6.5" - resolved "https://registry.yarnpkg.com/imports-loader/-/imports-loader-0.6.5.tgz#ae74653031d59e37b3c2fb2544ac61aeae3530a6" - dependencies: - loader-utils "0.2.x" - source-map "0.1.x" - imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -2624,17 +2617,17 @@ jodid25519@^1.0.0: dependencies: jsbn "~0.1.0" -"jquery-ui@github:jquery/jquery-ui#1.11.4": +"jquery-ui@git+https://github.com/jquery/jquery-ui#1.11.4": version "1.11.4" - resolved "https://codeload.github.com/jquery/jquery-ui/tar.gz/d6713024e16de90ea71dc0544ba34e1df01b4d8a" + resolved "git+https://github.com/jquery/jquery-ui#d6713024e16de90ea71dc0544ba34e1df01b4d8a" -jquery-ujs@1.2.1: +jquery-ujs@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/jquery-ujs/-/jquery-ujs-1.2.1.tgz#6ee75b1ef4e9ac95e7124f8d71f7d351f5548e92" dependencies: jquery ">=1.8.0" -jquery@2.2.1, jquery@>=1.8.0: +jquery@^2.2.1, jquery@>=1.8.0: version "2.2.1" resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.1.tgz#3c3e16854ad3d2ac44ac65021b17426d22ad803f" @@ -3031,7 +3024,7 @@ moment@2.x: version "2.17.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82" -mousetrap@1.4.6: +mousetrap@^1.4.6: version "1.4.6" resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.4.6.tgz#eaca72e22e56d5b769b7555873b688c3332e390a" @@ -4296,7 +4289,7 @@ unc-path-regex@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" -underscore@1.8.3: +underscore@^1.8.3: version "1.8.3" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" @@ -4387,11 +4380,11 @@ void-elements@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" -vue-resource@0.9.3: +vue-resource@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-0.9.3.tgz#ab46e1c44ea219142dcc28ae4043b3b04c80959d" -vue@2.0.3: +vue@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/vue/-/vue-2.0.3.tgz#3f7698f83d6ad1f0e35955447901672876c63fde"