diff options
575 files changed, 18199 insertions, 15479 deletions
diff --git a/.gitattributes b/.gitattributes index 7e800609e6c..17cbaa5eef5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ -CHANGELOG merge=union
\ No newline at end of file +CHANGELOG merge=union +*.js.es6 gitlab-language=javascript diff --git a/CHANGELOG b/CHANGELOG index e1145f36ce3..df54682dd10 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,35 +1,89 @@ Please view this file on the master branch, on stable branches it's out of date. + v 8.11.0 (unreleased) + - Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell) + - Fix CI status icon link underline (ClemMakesApps) - Fix of 'Commits being passed to custom hooks are already reachable when using the UI' + - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable + - Optimize maximum user access level lookup in loading of notes - Limit git rev-list output count to one in forced push check - -v 8.10.0 (unreleased) + - Clean up unused routes (Josef Strzibny) + - Add green outline to New Branch button. !5447 (winniehell) + - Retrieve rendered HTML from cache in one request + - Nokogiri's various parsing methods are now instrumented + - Add build event color in HipChat messages (David Eisner) + - Make fork counter always clickable. !5463 (winniehell) + - Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le) + - Load project invited groups and members eagerly in `ProjectTeam#fetch_members` + - Make branches sortable without push permission !5462 (winniehell) + - Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska) + - Add the `sprockets-es6` gem + - Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska) + - Profile requests when a header is passed + - Make error pages responsive (Takuya Noguchi) + +v 8.10.3 (unreleased) + +v 8.10.2 + - User can now search branches by name. !5144 + - Page is now properly rendered after committing the first file and creating the first branch. !5399 + - Add branch or tag icon to ref in builds page. !5434 + - Fix backup restore. !5459 + - Use project ID in repository cache to prevent stale data from persisting across projects. !5460 + - Fix issue with autocomplete search not working with enter key. !5466 + - Add iid to MR API response. !5468 + - Disable MySQL foreign key checks before dropping all tables. !5472 + - Ensure relative paths for video are rewritten as we do for images. !5474 + - Ensure current user can retry a build before showing the 'Retry' button. !5476 + - Add ENV variable to skip repository storages validations. !5478 + - Added `*.js.es6 gitlab-language=javascript` to `.gitattributes`. !5486 + - Don't show comment button in gutter of diffs on MR discussion tab. !5493 + - Rescue Rugged::OSError (lock exists) when creating references. !5497 + - Fix expand all diffs button in compare view. !5500 + - Show release notes in tags list. !5503 + - Fix a bug where forking a project from a repository storage to another would fail. !5509 + - Fix missing schema update for `20160722221922`. !5512 + - Update `gitlab-shell` version to 3.2.1 in the 8.9->8.10 update guide. !5516 + +v 8.10.1 + - Refactor repository storages documentation. !5428 + - Gracefully handle case when keep-around references are corrupted or exist already. !5430 + - Add detailed info on storage path mountpoints. !5437 + - Fix Error 500 when creating Wiki pages with hyphens or spaces. !5444 + - Fix bug where replies to commit notes displayed in the MR discussion tab wouldn't show up on the commit page. !5446 + - Ignore invalid trusted proxies in X-Forwarded-For header. !5454 + - Add links to the real markdown.md file for all GFM examples. !5458 + +v 8.10.0 - Fix profile activity heatmap to show correct day name (eanplatter) - Speed up ExternalWikiHelper#get_project_wiki_path - Expose {should,force}_remove_source_branch (Ben Boeckel) - - Add the functionality to be able to rename a file. !5049 (tiagonbotelho) + - Add the functionality to be able to rename a file. !5049 - Disable PostgreSQL statement timeout during migrations - - Fix projects dropdown loading performance with a simplified api cal. !5113 (tiagonbotelho) + - Fix projects dropdown loading performance with a simplified api cal. !5113 - Fix commit builds API, return all builds for all pipelines for given commit. !4849 - Replace Haml with Hamlit to make view rendering faster. !3666 - Refresh the branch cache after `git gc` runs - Allow to disable request access button on projects/groups - Refactor repository paths handling to allow multiple git mount points - - Optimize system note visibility checking by memoizing the visible reference count !5070 + - Optimize system note visibility checking by memoizing the visible reference count. !5070 - Add Application Setting to configure default Repository Path for new projects - Delete award emoji when deleting a user - - Remove pinTo from Flash and make inline flash messages look nicer !4854 (winniehell) - - Add link to profile to commit avatar !5163 (winniehell) + - Remove pinTo from Flash and make inline flash messages look nicer. !4854 (winniehell) + - Add an API for downloading latest successful build from a particular branch or tag. !5347 + - Avoid data-integrity issue when cleaning up repository archive cache. + - Add link to profile to commit avatar. !5163 (winniehell) - Wrap code blocks on Activies and Todos page. !4783 (winniehell) - - Align flash messages with left side of page content !4959 (winniehell) - - Display tooltip for "Copy to Clipboard" button !5164 (winniehell) - - Use default cursor for table header of project files !5165 (winniehell) + - Align flash messages with left side of page content. !4959 (winniehell) + - Display tooltip for "Copy to Clipboard" button. !5164 (winniehell) + - Use default cursor for table header of project files. !5165 (winniehell) - Store when and yaml variables in builds table - - Display last commit of deleted branch in push events !4699 (winniehell) - - Escape file extension when parsing search results !5141 (winniehell) + - Display last commit of deleted branch in push events. !4699 (winniehell) + - Escape file extension when parsing search results. !5141 (winniehell) - Add "passing with warnings" to the merge request pipeline possible statuses, this happens when builds that allow failures have failed. !5004 + - Add image border in Markdown preview. !5162 (winniehell) - Apply the trusted_proxies config to the rack request object for use with rack_attack - - Added the ability to block sign ups using a domain blacklist !5259 + - Added the ability to block sign ups using a domain blacklist. !5259 - Upgrade to Rails 4.2.7. !5236 - Extend exposed environment variables for CI builds - Deprecate APIs "projects/:id/keys/...". Use "projects/:id/deploy_keys/..." instead @@ -37,35 +91,36 @@ v 8.10.0 (unreleased) - Allow to pull code with deploy key from public projects - Use limit parameter rather than hardcoded value in `ldap:check` rake task (Mike Ricketts) - Add Sidekiq queue duration to transaction metrics. - - Add a new column `artifacts_size` to table `ci_builds` !4964 + - Add a new column `artifacts_size` to table `ci_builds`. !4964 - Let Workhorse serve format-patch diffs - - Display tooltip for mentioned users and groups !5261 (winniehell) + - Display tooltip for mentioned users and groups. !5261 (winniehell) - Allow build email service to be tested - Added day name to contribution calendar tooltips - Refactor user authorization check for a single project to avoid querying all user projects - - Make images fit to the size of the viewport !4810 - - Fix check for New Branch button on Issue page !4630 (winniehell) + - Make images fit to the size of the viewport. !4810 + - Fix check for New Branch button on Issue page. !4630 (winniehell) - Fix GFM autocomplete not working on wiki pages + - Fixed enter key not triggering click on first row when searching in a dropdown - Fix MR-auto-close text added to description. !4836 - Support U2F devices in Firefox. !5177 - - Fix issue, preventing users w/o push access to sort tags !5105 (redetection) + - Fix issue, preventing users w/o push access to sort tags. !5105 (redetection) - Add Spring EmojiOne updates. - - Added Rake task for tracking deployments !5320 + - Added Rake task for tracking deployments. !5320 - Fix fetching LFS objects for private CI projects - Add the new 2016 Emoji! Adds 72 new emoji including bacon, facepalm, and selfie. !5237 - - Add syntax for multiline blockquote using `>>>` fence !3954 + - Add syntax for multiline blockquote using `>>>` fence. !3954 - Fix viewing notification settings when a project is pending deletion - Updated compare dropdown menus to use GL dropdown - Redirects back to issue after clicking login link - Eager load award emoji on notes - Allow to define manual actions/builds on Pipelines and Environments - Fix pagination when sorting by columns with lots of ties (like priority) - - The Markdown reference parsers now re-use query results to prevent running the same queries multiple times !5020 + - The Markdown reference parsers now re-use query results to prevent running the same queries multiple times. !5020 - Updated project header design - Issuable collapsed assignee tooltip is now the users name - Fix compare view not changing code view rendering style - Exclude email check from the standard health check - - Updated layout for Projects, Groups, Users on Admin area !4424 + - Updated layout for Projects, Groups, Users on Admin area. !4424 - Fix changing issue state columns in milestone view - Update health_check gem to version 2.1.0 - Add notification settings dropdown for groups @@ -73,22 +128,23 @@ v 8.10.0 (unreleased) - Wildcards for protected branches. !4665 - Allow importing from Github using Personal Access Tokens. (Eric K Idema) - API: Expose `due_date` for issues (Robert Schilling) - - API: Todos !3188 (Robert Schilling) - - API: Expose shared groups for projects and shared projects for groups !5050 (Robert Schilling) - - API: Expose `developers_can_push` and `developers_can_merge` for branches !5208 (Robert Schilling) + - API: Todos. !3188 (Robert Schilling) + - API: Expose shared groups for projects and shared projects for groups. !5050 (Robert Schilling) + - API: Expose `developers_can_push` and `developers_can_merge` for branches. !5208 (Robert Schilling) - Add "Enabled Git access protocols" to Application Settings - Diffs will create button/diff form on demand no on server side - Reduce size of HTML used by diff comment forms - Protected branches have a "Developers can Merge" setting. !4892 (original implementation by Mathias Vestergaard) - - Fix user creation with stronger minimum password requirements !4054 (nathan-pmt) + - Fix user creation with stronger minimum password requirements. !4054 (nathan-pmt) - Only show New Snippet button to users that can create snippets. - PipelinesFinder uses git cache data - Track a user who created a pipeline - Actually render old and new sections of parallel diff next to each other - Throttle the update of `project.pushes_since_gc` to 1 minute. - - Allow expanding and collapsing files in diff view (!4990) + - Allow expanding and collapsing files in diff view. !4990 - Collapse large diffs by default (!4990) - Fix mentioned users list on diff notes + - Add support for inline videos in GitLab Flavored Markdown. !5215 (original implementation by Eric Hayes) - Fix creation of deployment on build that is retried, redeployed or rollback - Don't parse Rinku returned value to DocFragment when it didn't change the original html string. - Check for conflicts with existing Project's wiki path when creating a new project. @@ -101,8 +157,9 @@ v 8.10.0 (unreleased) - ObjectRenderer retrieve renderer content using Rails.cache.read_multi - Better caching of git calls on ProjectsController#show. - Avoid to retrieve MR closes_issues as much as possible. - - Add API endpoint for a group issues !4520 (mahcsig) - - Add Bugzilla integration !4930 (iamtjg) + - Hide project name in project activities. !5068 (winniehell) + - Add API endpoint for a group issues. !4520 (mahcsig) + - Add Bugzilla integration. !4930 (iamtjg) - Fix new snippet style bug (elliotec) - Instrument Rinku usage - Be explicit to define merge request discussion variables @@ -124,10 +181,10 @@ v 8.10.0 (unreleased) - Fix importer for GitHub Pull Requests when a branch was reused across Pull Requests - Add date when user joined the team on the member page - Fix 404 redirect after validation fails importing a GitLab project - - Added setting to set new users by default as external !4545 (Dravere) - - Add min value for project limit field on user's form !3622 (jastkand) + - Added setting to set new users by default as external. !4545 (Dravere) + - Add min value for project limit field on user's form. !3622 (jastkand) - Reset project pushes_since_gc when we enqueue the git gc call - - Add reminder to not paste private SSH keys !4399 (Ingo Blechschmidt) + - Add reminder to not paste private SSH keys. !4399 (Ingo Blechschmidt) - Collapsed diffs lines/size don't acumulate to overflow diffs. - Remove duplicate `description` field in `MergeRequest` entities (Ben Boeckel) - Style of import project buttons were fixed in the new project page. !5183 (rdemirbay) @@ -141,7 +198,7 @@ v 8.10.0 (unreleased) - Fix last update timestamp on issues not preserved on gitlab.com and project imports - Fix issues importing projects from EE to CE - Fix creating group with space in group path - - Improve cron_jobs loading error messages !5318 / !5360 + - Improve cron_jobs loading error messages. !5318 / !5360 - Prevent toggling sidebar when clipboard icon clicked - Create Todos for Issue author when assign or mention himself (Katarzyna Kobierska) - Limit the number of retries on error to 3 for exporting projects @@ -153,6 +210,7 @@ v 8.10.0 (unreleased) - Add builds badge to Pipelines settings page - Export and import avatar as part of project import/export - Fix migration corrupting import data for old version upgrades + - Show tooltip on GitLab export link in new project page v 8.9.6 - Fix importing of events under notes for GitLab projects. !5154 @@ -255,6 +313,7 @@ v 8.9.1 - Remove width restriction for logo on sign-in page. !4888 - Bump gitlab_git to 10.2.3 to fix false truncated warnings with ISO-8559 files. !4884 - Apply selected value as label. !4886 + - Change Retry to Re-deploy on Deployments page - Fix temp file being deleted after the request while importing a GitLab project. !4894 - Fix pagination when sorting by columns with lots of ties (like priority) - Implement Subresource Integrity for CSS and JavaScript assets. This prevents malicious assets from loading in the case of a CDN compromise. diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 944880fa15e..e4604e3afd0 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -3.2.0 +3.2.1 @@ -9,6 +9,7 @@ gem 'responders', '~> 2.0' # Specify a sprockets version due to increased performance # See https://gitlab.com/gitlab-org/gitlab-ce/issues/6069 gem 'sprockets', '~> 3.6.0' +gem 'sprockets-es6' # Default values for AR models gem 'default_value_for', '~> 3.0.0' @@ -274,7 +275,7 @@ group :development, :test do gem 'awesome_print', '~> 1.2.0', require: false gem 'fuubar', '~> 2.0.0' - gem 'database_cleaner', '~> 1.4.0' + gem 'database_cleaner', '~> 1.5.0' gem 'factory_girl_rails', '~> 4.6.0' gem 'rspec-rails', '~> 3.5.0' gem 'rspec-retry', '~> 0.4.5' @@ -333,6 +334,8 @@ gem 'mail_room', '~> 0.8' gem 'email_reply_parser', '~> 0.5.8' +gem 'ruby-prof', '~> 0.15.9' + ## CI gem 'activerecord-session_store', '~> 1.0.0' gem 'nested_form', '~> 0.3.2' @@ -347,5 +350,5 @@ gem 'paranoia', '~> 2.0' gem 'health_check', '~> 2.1.0' # System information -gem 'vmstat', '~> 2.1.0' +gem 'vmstat', '~> 2.1.1' gem 'sys-filesystem', '~> 1.1.6' diff --git a/Gemfile.lock b/Gemfile.lock index b53deb8c233..6fc6219bb67 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -85,6 +85,10 @@ GEM faraday (~> 0.9) faraday_middleware (~> 0.10) nokogiri (~> 1.6) + babel-source (5.8.35) + babel-transpiler (0.7.0) + babel-source (>= 4.0, < 6) + execjs (~> 2.0) babosa (1.0.2) base32 (0.3.2) bcrypt (3.1.11) @@ -149,7 +153,7 @@ GEM d3_rails (3.5.11) railties (>= 3.1.0) daemons (1.2.3) - database_cleaner (1.4.1) + database_cleaner (1.5.3) debug_inspector (0.0.2) debugger-ruby_core_source (1.3.8) default_value_for (3.0.1) @@ -616,6 +620,7 @@ GEM rubocop (>= 0.40.0) ruby-fogbugz (0.2.1) crack (~> 0.4) + ruby-prof (0.15.9) ruby-progressbar (1.8.1) ruby-saml (1.3.0) nokogiri (>= 1.5.10) @@ -700,6 +705,10 @@ GEM sprockets (3.6.3) concurrent-ruby (~> 1.0) rack (> 1, < 3) + sprockets-es6 (0.9.0) + babel-source (>= 5.8.11) + babel-transpiler + sprockets (>= 3.0.0) sprockets-rails (3.1.1) actionpack (>= 4.0) activesupport (>= 4.0) @@ -775,7 +784,7 @@ GEM coercible (~> 1.0) descendants_tracker (~> 0.0, >= 0.0.3) equalizer (~> 0.0, >= 0.0.9) - vmstat (2.1.0) + vmstat (2.1.1) warden (1.2.6) rack (>= 1.0) web-console (2.3.0) @@ -833,7 +842,7 @@ DEPENDENCIES connection_pool (~> 2.0) creole (~> 0.5.0) d3_rails (~> 3.5.0) - database_cleaner (~> 1.4.0) + database_cleaner (~> 1.5.0) default_value_for (~> 3.0.0) devise (~> 4.0) devise-two-factor (~> 3.0.0) @@ -940,6 +949,7 @@ DEPENDENCIES rubocop (~> 0.41.2) rubocop-rspec (~> 1.5.0) ruby-fogbugz (~> 0.2.1) + ruby-prof (~> 0.15.9) sanitize (~> 2.0) sass-rails (~> 5.0.0) scss_lint (~> 0.47.0) @@ -963,6 +973,7 @@ DEPENDENCIES spring-commands-spinach (~> 1.1.0) spring-commands-teaspoon (~> 0.0.2) sprockets (~> 3.6.0) + sprockets-es6 state_machines-activerecord (~> 0.4.0) sys-filesystem (~> 1.1.6) task_list (~> 1.0.2) @@ -980,7 +991,7 @@ DEPENDENCIES unicorn-worker-killer (~> 0.4.2) version_sorter (~> 2.0.0) virtus (~> 1.0.1) - vmstat (~> 2.1.0) + vmstat (~> 2.1.1) web-console (~> 2.0) webmock (~> 1.21.0) wikicloth (= 0.8.1) @@ -1 +1 @@ -8.10.0-pre +8.11.0-pre diff --git a/app/assets/javascripts/LabelManager.js b/app/assets/javascripts/LabelManager.js new file mode 100644 index 00000000000..151455ce4a3 --- /dev/null +++ b/app/assets/javascripts/LabelManager.js @@ -0,0 +1,110 @@ +(function() { + this.LabelManager = (function() { + LabelManager.prototype.errorMessage = 'Unable to update label prioritization at this time'; + + function LabelManager(opts) { + var ref, ref1, ref2; + if (opts == null) { + opts = {}; + } + this.togglePriorityButton = (ref = opts.togglePriorityButton) != null ? ref : $('.js-toggle-priority'), this.prioritizedLabels = (ref1 = opts.prioritizedLabels) != null ? ref1 : $('.js-prioritized-labels'), this.otherLabels = (ref2 = opts.otherLabels) != null ? ref2 : $('.js-other-labels'); + this.prioritizedLabels.sortable({ + items: 'li', + placeholder: 'list-placeholder', + axis: 'y', + update: this.onPrioritySortUpdate.bind(this) + }); + this.bindEvents(); + } + + LabelManager.prototype.bindEvents = function() { + return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick); + }; + + LabelManager.prototype.onTogglePriorityClick = function(e) { + var $btn, $label, $tooltip, _this, action; + e.preventDefault(); + _this = e.data; + $btn = $(e.currentTarget); + $label = $("#" + ($btn.data('domId'))); + action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; + $tooltip = $("#" + ($btn.find('.has-tooltip:visible').attr('aria-describedby'))); + $tooltip.tooltip('destroy'); + return _this.toggleLabelPriority($label, action); + }; + + LabelManager.prototype.toggleLabelPriority = function($label, action, persistState) { + var $from, $target, _this, url, xhr; + if (persistState == null) { + persistState = true; + } + _this = this; + url = $label.find('.js-toggle-priority').data('url'); + $target = this.prioritizedLabels; + $from = this.otherLabels; + if (action === 'remove') { + $target = this.otherLabels; + $from = this.prioritizedLabels; + } + if ($from.find('li').length === 1) { + $from.find('.empty-message').removeClass('hidden'); + } + if (!$target.find('li').length) { + $target.find('.empty-message').addClass('hidden'); + } + $label.detach().appendTo($target); + if (!persistState) { + return; + } + if (action === 'remove') { + xhr = $.ajax({ + url: url, + type: 'DELETE' + }); + if (!$from.find('li').length) { + $from.find('.empty-message').removeClass('hidden'); + } + } else { + xhr = this.savePrioritySort($label, action); + } + return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action)); + }; + + LabelManager.prototype.onPrioritySortUpdate = function() { + var xhr; + xhr = this.savePrioritySort(); + return xhr.fail(function() { + return new Flash(this.errorMessage, 'alert'); + }); + }; + + LabelManager.prototype.savePrioritySort = function() { + return $.post({ + url: this.prioritizedLabels.data('url'), + data: { + label_ids: this.getSortedLabelsIds() + } + }); + }; + + LabelManager.prototype.rollbackLabelPosition = function($label, originalAction) { + var action; + action = originalAction === 'remove' ? 'add' : 'remove'; + this.toggleLabelPriority($label, action, false); + return new Flash(this.errorMessage, 'alert'); + }; + + LabelManager.prototype.getSortedLabelsIds = function() { + var sortedIds; + sortedIds = []; + this.prioritizedLabels.find('li').each(function() { + return sortedIds.push($(this).data('id')); + }); + return sortedIds; + }; + + return LabelManager; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/LabelManager.js.coffee b/app/assets/javascripts/LabelManager.js.coffee deleted file mode 100644 index 6d8faba40d7..00000000000 --- a/app/assets/javascripts/LabelManager.js.coffee +++ /dev/null @@ -1,92 +0,0 @@ -class @LabelManager - errorMessage: 'Unable to update label prioritization at this time' - - constructor: (opts = {}) -> - # Defaults - { - @togglePriorityButton = $('.js-toggle-priority') - @prioritizedLabels = $('.js-prioritized-labels') - @otherLabels = $('.js-other-labels') - } = opts - - @prioritizedLabels.sortable( - items: 'li' - placeholder: 'list-placeholder' - axis: 'y' - update: @onPrioritySortUpdate.bind(@) - ) - - @bindEvents() - - bindEvents: -> - @togglePriorityButton.on 'click', @, @onTogglePriorityClick - - onTogglePriorityClick: (e) -> - e.preventDefault() - _this = e.data - $btn = $(e.currentTarget) - $label = $("##{$btn.data('domId')}") - action = if $btn.parents('.js-prioritized-labels').length then 'remove' else 'add' - - # Make sure tooltip will hide - $tooltip = $ "##{$btn.find('.has-tooltip:visible').attr('aria-describedby')}" - $tooltip.tooltip 'destroy' - - _this.toggleLabelPriority($label, action) - - toggleLabelPriority: ($label, action, persistState = true) -> - _this = @ - url = $label.find('.js-toggle-priority').data 'url' - - $target = @prioritizedLabels - $from = @otherLabels - - # Optimistic update - if action is 'remove' - $target = @otherLabels - $from = @prioritizedLabels - - if $from.find('li').length is 1 - $from.find('.empty-message').removeClass('hidden') - - if not $target.find('li').length - $target.find('.empty-message').addClass('hidden') - - $label.detach().appendTo($target) - - # Return if we are not persisting state - return unless persistState - - if action is 'remove' - xhr = $.ajax url: url, type: 'DELETE' - - # Restore empty message - $from.find('.empty-message').removeClass('hidden') unless $from.find('li').length - else - xhr = @savePrioritySort($label, action) - - xhr.fail @rollbackLabelPosition.bind(@, $label, action) - - onPrioritySortUpdate: -> - xhr = @savePrioritySort() - - xhr.fail -> - new Flash(@errorMessage, 'alert') - - savePrioritySort: () -> - $.post - url: @prioritizedLabels.data('url') - data: - label_ids: @getSortedLabelsIds() - - rollbackLabelPosition: ($label, originalAction)-> - action = if originalAction is 'remove' then 'add' else 'remove' - @toggleLabelPriority($label, action, false) - - new Flash(@errorMessage, 'alert') - - getSortedLabelsIds: -> - sortedIds = [] - @prioritizedLabels.find('li').each -> - sortedIds.push $(@).data 'id' - sortedIds diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js new file mode 100644 index 00000000000..1ab3c2197d8 --- /dev/null +++ b/app/assets/javascripts/activities.js @@ -0,0 +1,40 @@ +(function() { + this.Activities = (function() { + function Activities() { + Pager.init(20, true, false, this.updateTooltips); + $(".event-filter-link").on("click", (function(_this) { + return function(event) { + event.preventDefault(); + _this.toggleFilter($(event.currentTarget)); + return _this.reloadActivities(); + }; + })(this)); + } + + Activities.prototype.updateTooltips = function() { + return gl.utils.localTimeAgo($('.js-timeago', '#activity')); + }; + + Activities.prototype.reloadActivities = function() { + $(".content_list").html(''); + return Pager.init(20, true); + }; + + Activities.prototype.toggleFilter = function(sender) { + var event_filters, filter; + $('.event-filter .active').removeClass("active"); + event_filters = $.cookie("event_filter"); + filter = sender.attr("id").split("_")[0]; + $.cookie("event_filter", (event_filters !== filter ? filter : ""), { + path: '/' + }); + if (event_filters !== filter) { + return sender.closest('li').toggleClass("active"); + } + }; + + return Activities; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee deleted file mode 100644 index ed5a5d0260c..00000000000 --- a/app/assets/javascripts/activities.js.coffee +++ /dev/null @@ -1,24 +0,0 @@ -class @Activities - constructor: -> - Pager.init 20, true, false, @updateTooltips - $(".event-filter-link").on "click", (event) => - event.preventDefault() - @toggleFilter($(event.currentTarget)) - @reloadActivities() - - updateTooltips: -> - gl.utils.localTimeAgo($('.js-timeago', '#activity')) - - reloadActivities: -> - $(".content_list").html '' - Pager.init 20, true - - - toggleFilter: (sender) -> - $('.event-filter .active').removeClass "active" - event_filters = $.cookie("event_filter") - filter = sender.attr("id").split("_")[0] - $.cookie "event_filter", (if event_filters isnt filter then filter else ""), { path: '/' } - - if event_filters isnt filter - sender.closest('li').toggleClass "active" diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js new file mode 100644 index 00000000000..f8460beb5d2 --- /dev/null +++ b/app/assets/javascripts/admin.js @@ -0,0 +1,64 @@ +(function() { + this.Admin = (function() { + function Admin() { + var modal, showBlacklistType; + $('input#user_force_random_password').on('change', function(elem) { + var elems; + elems = $('#user_password, #user_password_confirmation'); + if ($(this).attr('checked')) { + return elems.val('').attr('disabled', true); + } else { + return elems.removeAttr('disabled'); + } + }); + $('body').on('click', '.js-toggle-colors-link', function(e) { + e.preventDefault(); + return $('.js-toggle-colors-container').toggle(); + }); + $('.log-tabs a').click(function(e) { + e.preventDefault(); + return $(this).tab('show'); + }); + $('.log-bottom').click(function(e) { + var visible_log; + e.preventDefault(); + visible_log = $(".file-content:visible"); + return visible_log.animate({ + scrollTop: visible_log.find('ol').height() + }, "fast"); + }); + modal = $('.change-owner-holder'); + $('.change-owner-link').bind("click", function(e) { + e.preventDefault(); + $(this).hide(); + return modal.show(); + }); + $('.change-owner-cancel-link').bind("click", function(e) { + e.preventDefault(); + modal.hide(); + return $('.change-owner-link').show(); + }); + $('li.project_member').bind('ajax:success', function() { + return Turbolinks.visit(location.href); + }); + $('li.group_member').bind('ajax:success', function() { + return Turbolinks.visit(location.href); + }); + showBlacklistType = function() { + if ($("input[name='blacklist_type']:checked").val() === 'file') { + $('.blacklist-file').show(); + return $('.blacklist-raw').hide(); + } else { + $('.blacklist-file').hide(); + return $('.blacklist-raw').show(); + } + }; + $("input[name='blacklist_type']").click(showBlacklistType); + showBlacklistType(); + } + + return Admin; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/admin.js.coffee b/app/assets/javascripts/admin.js.coffee deleted file mode 100644 index 90c09619f8c..00000000000 --- a/app/assets/javascripts/admin.js.coffee +++ /dev/null @@ -1,51 +0,0 @@ -class @Admin - constructor: -> - $('input#user_force_random_password').on 'change', (elem) -> - elems = $('#user_password, #user_password_confirmation') - - if $(@).attr 'checked' - elems.val('').attr 'disabled', true - else - elems.removeAttr 'disabled' - - $('body').on 'click', '.js-toggle-colors-link', (e) -> - e.preventDefault() - $('.js-toggle-colors-container').toggle() - - $('.log-tabs a').click (e) -> - e.preventDefault() - $(this).tab('show') - - $('.log-bottom').click (e) -> - e.preventDefault() - visible_log = $(".file-content:visible") - visible_log.animate({ scrollTop: visible_log.find('ol').height() }, "fast") - - modal = $('.change-owner-holder') - - $('.change-owner-link').bind "click", (e) -> - e.preventDefault() - $(this).hide() - modal.show() - - $('.change-owner-cancel-link').bind "click", (e) -> - e.preventDefault() - modal.hide() - $('.change-owner-link').show() - - $('li.project_member').bind 'ajax:success', -> - Turbolinks.visit(location.href) - - $('li.group_member').bind 'ajax:success', -> - Turbolinks.visit(location.href) - - showBlacklistType = -> - if $("input[name='blacklist_type']:checked").val() == 'file' - $('.blacklist-file').show() - $('.blacklist-raw').hide() - else - $('.blacklist-file').hide() - $('.blacklist-raw').show() - - $("input[name='blacklist_type']").click showBlacklistType - showBlacklistType() diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js new file mode 100644 index 00000000000..49c2ac0dac3 --- /dev/null +++ b/app/assets/javascripts/api.js @@ -0,0 +1,136 @@ +(function() { + this.Api = { + groupsPath: "/api/:version/groups.json", + groupPath: "/api/:version/groups/:id.json", + namespacesPath: "/api/:version/namespaces.json", + groupProjectsPath: "/api/:version/groups/:id/projects.json", + projectsPath: "/api/:version/projects.json?simple=true", + labelsPath: "/api/:version/projects/:id/labels", + licensePath: "/api/:version/licenses/:key", + gitignorePath: "/api/:version/gitignores/:key", + gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key", + group: function(group_id, callback) { + var url; + url = Api.buildUrl(Api.groupPath); + url = url.replace(':id', group_id); + return $.ajax({ + url: url, + data: { + private_token: gon.api_token + }, + dataType: "json" + }).done(function(group) { + return callback(group); + }); + }, + groups: function(query, skip_ldap, callback) { + var url; + url = Api.buildUrl(Api.groupsPath); + return $.ajax({ + url: url, + data: { + private_token: gon.api_token, + search: query, + per_page: 20 + }, + dataType: "json" + }).done(function(groups) { + return callback(groups); + }); + }, + namespaces: function(query, callback) { + var url; + url = Api.buildUrl(Api.namespacesPath); + return $.ajax({ + url: url, + data: { + private_token: gon.api_token, + search: query, + per_page: 20 + }, + dataType: "json" + }).done(function(namespaces) { + return callback(namespaces); + }); + }, + projects: function(query, order, callback) { + var url; + url = Api.buildUrl(Api.projectsPath); + return $.ajax({ + url: url, + data: { + private_token: gon.api_token, + search: query, + order_by: order, + per_page: 20 + }, + dataType: "json" + }).done(function(projects) { + return callback(projects); + }); + }, + newLabel: function(project_id, data, callback) { + var url; + url = Api.buildUrl(Api.labelsPath); + url = url.replace(':id', project_id); + data.private_token = gon.api_token; + return $.ajax({ + url: url, + type: "POST", + data: data, + dataType: "json" + }).done(function(label) { + return callback(label); + }).error(function(message) { + return callback(message.responseJSON); + }); + }, + groupProjects: function(group_id, query, callback) { + var url; + url = Api.buildUrl(Api.groupProjectsPath); + url = url.replace(':id', group_id); + return $.ajax({ + url: url, + data: { + private_token: gon.api_token, + search: query, + per_page: 20 + }, + dataType: "json" + }).done(function(projects) { + return callback(projects); + }); + }, + licenseText: function(key, data, callback) { + var url; + url = Api.buildUrl(Api.licensePath).replace(':key', key); + return $.ajax({ + url: url, + data: data + }).done(function(license) { + return callback(license); + }); + }, + gitignoreText: function(key, callback) { + var url; + url = Api.buildUrl(Api.gitignorePath).replace(':key', key); + return $.get(url, function(gitignore) { + return callback(gitignore); + }); + }, + gitlabCiYml: function(key, callback) { + var url; + url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key); + return $.get(url, function(file) { + return callback(file); + }); + }, + buildUrl: function(url) { + if (gon.relative_url_root != null) { + url = gon.relative_url_root + url; + } + return url.replace(':version', gon.api_version); + } + }; + +}).call(this); diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee deleted file mode 100644 index 89b0ac697ed..00000000000 --- a/app/assets/javascripts/api.js.coffee +++ /dev/null @@ -1,122 +0,0 @@ -@Api = - groupsPath: "/api/:version/groups.json" - groupPath: "/api/:version/groups/:id.json" - namespacesPath: "/api/:version/namespaces.json" - groupProjectsPath: "/api/:version/groups/:id/projects.json" - projectsPath: "/api/:version/projects.json?simple=true" - labelsPath: "/api/:version/projects/:id/labels" - licensePath: "/api/:version/licenses/:key" - gitignorePath: "/api/:version/gitignores/:key" - gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key" - - group: (group_id, callback) -> - url = Api.buildUrl(Api.groupPath) - url = url.replace(':id', group_id) - - $.ajax( - url: url - data: - private_token: gon.api_token - dataType: "json" - ).done (group) -> - callback(group) - - # Return groups list. Filtered by query - # Only active groups retrieved - groups: (query, skip_ldap, callback) -> - url = Api.buildUrl(Api.groupsPath) - - $.ajax( - url: url - data: - private_token: gon.api_token - search: query - per_page: 20 - dataType: "json" - ).done (groups) -> - callback(groups) - - # Return namespaces list. Filtered by query - namespaces: (query, callback) -> - url = Api.buildUrl(Api.namespacesPath) - - $.ajax( - url: url - data: - private_token: gon.api_token - search: query - per_page: 20 - dataType: "json" - ).done (namespaces) -> - callback(namespaces) - - # Return projects list. Filtered by query - projects: (query, order, callback) -> - url = Api.buildUrl(Api.projectsPath) - - $.ajax( - url: url - data: - private_token: gon.api_token - search: query - order_by: order - per_page: 20 - dataType: "json" - ).done (projects) -> - callback(projects) - - newLabel: (project_id, data, callback) -> - url = Api.buildUrl(Api.labelsPath) - url = url.replace(':id', project_id) - - data.private_token = gon.api_token - $.ajax( - url: url - type: "POST" - data: data - dataType: "json" - ).done (label) -> - callback(label) - .error (message) -> - callback(message.responseJSON) - - # Return group projects list. Filtered by query - groupProjects: (group_id, query, callback) -> - url = Api.buildUrl(Api.groupProjectsPath) - url = url.replace(':id', group_id) - - $.ajax( - url: url - data: - private_token: gon.api_token - search: query - per_page: 20 - dataType: "json" - ).done (projects) -> - callback(projects) - - # Return text for a specific license - licenseText: (key, data, callback) -> - url = Api.buildUrl(Api.licensePath).replace(':key', key) - - $.ajax( - url: url - data: data - ).done (license) -> - callback(license) - - gitignoreText: (key, callback) -> - url = Api.buildUrl(Api.gitignorePath).replace(':key', key) - - $.get url, (gitignore) -> - callback(gitignore) - - gitlabCiYml: (key, callback) -> - url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key) - - $.get url, (file) -> - callback(file) - - buildUrl: (url) -> - url = gon.relative_url_root + url if gon.relative_url_root? - return url.replace(':version', gon.api_version) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js new file mode 100644 index 00000000000..127e568adc9 --- /dev/null +++ b/app/assets/javascripts/application.js @@ -0,0 +1,320 @@ +/*= require jquery2 */ +/*= require jquery-ui/autocomplete */ +/*= require jquery-ui/datepicker */ +/*= require jquery-ui/draggable */ +/*= require jquery-ui/effect-highlight */ +/*= require jquery-ui/sortable */ +/*= require jquery_ujs */ +/*= require jquery.cookie */ +/*= require jquery.endless-scroll */ +/*= require jquery.highlight */ +/*= require jquery.waitforimages */ +/*= require jquery.atwho */ +/*= require jquery.scrollTo */ +/*= require jquery.turbolinks */ +/*= require turbolinks */ +/*= require autosave */ +/*= require bootstrap/affix */ +/*= require bootstrap/alert */ +/*= require bootstrap/button */ +/*= require bootstrap/collapse */ +/*= require bootstrap/dropdown */ +/*= require bootstrap/modal */ +/*= require bootstrap/scrollspy */ +/*= require bootstrap/tab */ +/*= require bootstrap/transition */ +/*= require bootstrap/tooltip */ +/*= require bootstrap/popover */ +/*= require select2 */ +/*= require ace/ace */ +/*= require ace/ext-searchbox */ +/*= require underscore */ +/*= require dropzone */ +/*= require mousetrap */ +/*= require mousetrap/pause */ +/*= require shortcuts */ +/*= require shortcuts_navigation */ +/*= require shortcuts_dashboard_navigation */ +/*= require shortcuts_issuable */ +/*= require shortcuts_network */ +/*= require jquery.nicescroll */ +/*= require date.format */ +/*= require_directory ./behaviors */ +/*= require_directory ./blob */ +/*= require_directory ./commit */ +/*= require_directory ./extensions */ +/*= require_directory ./lib/utils */ +/*= require_directory ./u2f */ +/*= require_directory . */ +/*= require fuzzaldrin-plus */ + +(function() { + window.slugify = function(text) { + return text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase(); + }; + + window.ajaxGet = function(url) { + return $.ajax({ + type: "GET", + url: url, + dataType: "script" + }); + }; + + window.split = function(val) { + return val.split(/,\s*/); + }; + + window.extractLast = function(term) { + return split(term).pop(); + }; + + window.rstrip = function(val) { + if (val) { + return val.replace(/\s+$/, ''); + } else { + return val; + } + }; + + window.disableButtonIfEmptyField = function(field_selector, button_selector) { + var closest_submit, field; + field = $(field_selector); + closest_submit = field.closest('form').find(button_selector); + if (rstrip(field.val()) === "") { + closest_submit.disable(); + } + return field.on('input', function() { + if (rstrip($(this).val()) === "") { + return closest_submit.disable(); + } else { + return closest_submit.enable(); + } + }); + }; + + window.disableButtonIfAnyEmptyField = function(form, form_selector, button_selector) { + var closest_submit, updateButtons; + closest_submit = form.find(button_selector); + updateButtons = function() { + var filled; + filled = true; + form.find('input').filter(form_selector).each(function() { + return filled = rstrip($(this).val()) !== "" || !$(this).attr('required'); + }); + if (filled) { + return closest_submit.enable(); + } else { + return closest_submit.disable(); + } + }; + updateButtons(); + return form.keyup(updateButtons); + }; + + window.sanitize = function(str) { + return str.replace(/<(?:.|\n)*?>/gm, ''); + }; + + window.unbindEvents = function() { + return $(document).off('scroll'); + }; + + window.shiftWindow = function() { + return scrollBy(0, -100); + }; + + document.addEventListener("page:fetch", unbindEvents); + + window.addEventListener("hashchange", shiftWindow); + + window.onload = function() { + if (location.hash) { + return setTimeout(shiftWindow, 100); + } + }; + + $(function() { + var $body, $document, $sidebarGutterToggle, $window, bootstrapBreakpoint, checkInitialSidebarSize, fitSidebarForSize, flash; + $document = $(document); + $window = $(window); + $body = $('body'); + gl.utils.preventDisabledButtons(); + bootstrapBreakpoint = bp.getBreakpointSize(); + $(".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(); + }); + }); + $('.remove-row').bind('ajax:success', function() { + return $(this).closest('li').fadeOut(); + }); + $('.js-remove-tr').bind('ajax:before', function() { + return $(this).hide(); + }); + $('.js-remove-tr').bind('ajax:success', function() { + return $(this).closest('tr').fadeOut(); + }); + $('select.select2').select2({ + width: 'resolve', + dropdownAutoWidth: true + }); + $('.js-select2').bind('select2-close', function() { + return setTimeout((function() { + $('.select2-container-active').removeClass('select2-container-active'); + return $(':focus').blur(); + }), 1); + }); + $body.tooltip({ + selector: '.has-tooltip, [data-toggle="tooltip"]', + placement: function(_, el) { + var $el; + $el = $(el); + return $el.data('placement') || 'bottom'; + } + }); + $('.trigger-submit').on('change', function() { + return $(this).parents('form').submit(); + }); + gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true); + if ((flash = $(".flash-container")).length > 0) { + flash.click(function() { + return $(this).fadeOut(); + }); + flash.show(); + } + $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function(e) { + var buttons; + buttons = $('[type="submit"]', this); + switch (e.type) { + case 'ajax:beforeSend': + case 'submit': + return buttons.disable(); + default: + return buttons.enable(); + } + }); + $(document).ajaxError(function(e, xhrObj, xhrSetting, xhrErrorText) { + var ref; + if (xhrObj.status === 401) { + return new Flash('You need to be logged in.', 'alert'); + } else if ((ref = xhrObj.status) === 404 || ref === 500) { + return new Flash('Something went wrong on our end.', 'alert'); + } + }); + $('.account-box').hover(function() { + return $(this).toggleClass('hover'); + }); + $document.on('click', '.diff-content .js-show-suppressed-diff', function() { + var $container; + $container = $(this).parent(); + $container.next('table').show(); + return $container.remove(); + }); + $('.navbar-toggle').on('click', function() { + $('.header-content .title').toggle(); + $('.header-content .header-logo').toggle(); + $('.header-content .navbar-collapse').toggle(); + return $('.navbar-toggle').toggleClass('active'); + }); + $body.on("click", ".js-toggle-diff-comments", function(e) { + $(this).toggleClass('active'); + $(this).closest(".diff-file").find(".notes_holder").toggle(); + return e.preventDefault(); + }); + $document.off("click", '.js-confirm-danger'); + $document.on("click", '.js-confirm-danger', function(e) { + var btn, form, text; + e.preventDefault(); + btn = $(e.target); + text = btn.data("confirm-danger-message"); + form = btn.closest("form"); + return new ConfirmDangerModal(form, text); + }); + $document.on('click', 'button', function() { + return $(this).blur(); + }); + $('input[type="search"]').each(function() { + var $this; + $this = $(this); + $this.attr('value', $this.val()); + }); + $document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function(e) { + var $this; + $this = $(this); + return $this.attr('value', $this.val()); + }); + $sidebarGutterToggle = $('.js-sidebar-toggle'); + $document.off('breakpoint:change').on('breakpoint:change', function(e, breakpoint) { + var $gutterIcon; + if (breakpoint === 'sm' || breakpoint === 'xs') { + $gutterIcon = $sidebarGutterToggle.find('i'); + if ($gutterIcon.hasClass('fa-angle-double-right')) { + return $sidebarGutterToggle.trigger('click'); + } + } + }); + fitSidebarForSize = function() { + var oldBootstrapBreakpoint; + oldBootstrapBreakpoint = bootstrapBreakpoint; + bootstrapBreakpoint = bp.getBreakpointSize(); + if (bootstrapBreakpoint !== oldBootstrapBreakpoint) { + return $document.trigger('breakpoint:change', [bootstrapBreakpoint]); + } + }; + checkInitialSidebarSize = function() { + bootstrapBreakpoint = bp.getBreakpointSize(); + if (bootstrapBreakpoint === "xs" || "sm") { + return $document.trigger('breakpoint:change', [bootstrapBreakpoint]); + } + }; + $window.off("resize.app").on("resize.app", function(e) { + return fitSidebarForSize(); + }); + gl.awardsHandler = new AwardsHandler(); + checkInitialSidebarSize(); + new Aside(); + if ($window.width() < 1024 && $.cookie('pin_nav') === 'true') { + $.cookie('pin_nav', 'false', { + path: '/', + expires: 365 * 10 + }); + $('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded').removeClass('page-sidebar-pinned'); + $('.navbar-fixed-top').removeClass('header-pinned-nav'); + } + return $document.off('click', '.js-nav-pin').on('click', '.js-nav-pin', function(e) { + var $page, $pinBtn, $tooltip, $topNav, doPinNav, tooltipText; + e.preventDefault(); + $pinBtn = $(e.currentTarget); + $page = $('.page-with-sidebar'); + $topNav = $('.navbar-fixed-top'); + $tooltip = $("#" + ($pinBtn.attr('aria-describedby'))); + doPinNav = !$page.is('.page-sidebar-pinned'); + tooltipText = 'Pin navigation'; + $(this).toggleClass('is-active'); + if (doPinNav) { + $page.addClass('page-sidebar-pinned'); + $topNav.addClass('header-pinned-nav'); + } else { + $tooltip.remove(); + $page.removeClass('page-sidebar-pinned').toggleClass('page-sidebar-collapsed page-sidebar-expanded'); + $topNav.removeClass('header-pinned-nav').toggleClass('header-collapsed header-expanded'); + } + $.cookie('pin_nav', doPinNav, { + path: '/', + expires: 365 * 10 + }); + if ($.cookie('pin_nav') === 'true' || doPinNav) { + tooltipText = 'Unpin navigation'; + } + $tooltip.find('.tooltip-inner').text(tooltipText); + return $pinBtn.attr('title', tooltipText).tooltip('fixTitle'); + }); + }); + +}).call(this); diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee deleted file mode 100644 index eceff6d91d5..00000000000 --- a/app/assets/javascripts/application.js.coffee +++ /dev/null @@ -1,310 +0,0 @@ -# This is a manifest file that'll be compiled into including all the files listed below. -# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically -# be included in the compiled file accessible from http://example.com/assets/application.js -# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -# the compiled file. -# -#= require jquery2 -#= require jquery-ui/autocomplete -#= require jquery-ui/datepicker -#= require jquery-ui/draggable -#= require jquery-ui/effect-highlight -#= require jquery-ui/sortable -#= require jquery_ujs -#= require jquery.cookie -#= require jquery.endless-scroll -#= require jquery.highlight -#= require jquery.waitforimages -#= require jquery.atwho -#= require jquery.scrollTo -#= require jquery.turbolinks -#= require turbolinks -#= require autosave -#= require bootstrap/affix -#= require bootstrap/alert -#= require bootstrap/button -#= require bootstrap/collapse -#= require bootstrap/dropdown -#= require bootstrap/modal -#= require bootstrap/scrollspy -#= require bootstrap/tab -#= require bootstrap/transition -#= require bootstrap/tooltip -#= require bootstrap/popover -#= require select2 -#= require ace/ace -#= require ace/ext-searchbox -#= require underscore -#= require dropzone -#= require mousetrap -#= require mousetrap/pause -#= require shortcuts -#= require shortcuts_navigation -#= require shortcuts_dashboard_navigation -#= require shortcuts_issuable -#= require shortcuts_network -#= require jquery.nicescroll -#= require date.format -#= require_directory ./behaviors -#= require_directory ./blob -#= require_directory ./commit -#= require_directory ./extensions -#= require_directory ./lib/utils -#= require_directory ./u2f -#= require_directory . -#= require fuzzaldrin-plus - -window.slugify = (text) -> - text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() - -window.ajaxGet = (url) -> - $.ajax({type: "GET", url: url, dataType: "script"}) - -window.split = (val) -> - return val.split( /,\s*/ ) - -window.extractLast = (term) -> - return split( term ).pop() - -window.rstrip = (val) -> - return if val then val.replace(/\s+$/, '') else val - -# Disable button if text field is empty -window.disableButtonIfEmptyField = (field_selector, button_selector) -> - field = $(field_selector) - closest_submit = field.closest('form').find(button_selector) - - closest_submit.disable() if rstrip(field.val()) is "" - - field.on 'input', -> - if rstrip($(@).val()) is "" - closest_submit.disable() - else - closest_submit.enable() - -# Disable button if any input field with given selector is empty -window.disableButtonIfAnyEmptyField = (form, form_selector, button_selector) -> - closest_submit = form.find(button_selector) - updateButtons = -> - filled = true - form.find('input').filter(form_selector).each -> - filled = rstrip($(this).val()) != "" || !$(this).attr('required') - - if filled - closest_submit.enable() - else - closest_submit.disable() - - updateButtons() - form.keyup(updateButtons) - -window.sanitize = (str) -> - return str.replace(/<(?:.|\n)*?>/gm, '') - -window.unbindEvents = -> - $(document).off('scroll') - -window.shiftWindow = -> - scrollBy 0, -100 - -document.addEventListener("page:fetch", unbindEvents) - -window.addEventListener "hashchange", shiftWindow - -window.onload = -> - # Scroll the window to avoid the topnav bar - # https://github.com/twitter/bootstrap/issues/1768 - if location.hash - setTimeout shiftWindow, 100 - -$ -> - - $document = $(document) - $window = $(window) - $body = $('body') - - gl.utils.preventDisabledButtons() - bootstrapBreakpoint = bp.getBreakpointSize() - - $(".nav-sidebar").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF") - - # Click a .js-select-on-focus field, select the contents - $(".js-select-on-focus").on "focusin", -> - # Prevent a mouseup event from deselecting the input - $(this).select().one 'mouseup', (e) -> - e.preventDefault() - - $('.remove-row').bind 'ajax:success', -> - $(this).closest('li').fadeOut() - - $('.js-remove-tr').bind 'ajax:before', -> - $(this).hide() - - $('.js-remove-tr').bind 'ajax:success', -> - $(this).closest('tr').fadeOut() - - # Initialize select2 selects - $('select.select2').select2(width: 'resolve', dropdownAutoWidth: true) - - # Close select2 on escape - $('.js-select2').bind 'select2-close', -> - setTimeout ( -> - $('.select2-container-active').removeClass('select2-container-active') - $(':focus').blur() - ), 1 - - # Initialize tooltips - $body.tooltip( - selector: '.has-tooltip, [data-toggle="tooltip"]' - placement: (_, el) -> - $el = $(el) - $el.data('placement') || 'bottom' - ) - - # Form submitter - $('.trigger-submit').on 'change', -> - $(@).parents('form').submit() - - gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true) - - # Flash - if (flash = $(".flash-container")).length > 0 - flash.click -> $(@).fadeOut() - flash.show() - - # Disable form buttons while a form is submitting - $body.on 'ajax:complete, ajax:beforeSend, submit', 'form', (e) -> - buttons = $('[type="submit"]', @) - - switch e.type - when 'ajax:beforeSend', 'submit' - buttons.disable() - else - buttons.enable() - - $(document).ajaxError (e, xhrObj, xhrSetting, xhrErrorText) -> - - if xhrObj.status is 401 - new Flash 'You need to be logged in.', 'alert' - - else if xhrObj.status in [ 404, 500 ] - new Flash 'Something went wrong on our end.', 'alert' - - - # Show/Hide the profile menu when hovering the account box - $('.account-box').hover -> $(@).toggleClass('hover') - - # Commit show suppressed diff - $document.on 'click', '.diff-content .js-show-suppressed-diff', -> - $container = $(@).parent() - $container.next('table').show() - $container.remove() - - $('.navbar-toggle').on 'click', -> - $('.header-content .title').toggle() - $('.header-content .header-logo').toggle() - $('.header-content .navbar-collapse').toggle() - $('.navbar-toggle').toggleClass('active') - - # Show/hide comments on diff - $body.on "click", ".js-toggle-diff-comments", (e) -> - $(@).toggleClass('active') - $(@).closest(".diff-file").find(".notes_holder").toggle() - e.preventDefault() - - $document.off "click", '.js-confirm-danger' - $document.on "click", '.js-confirm-danger', (e) -> - e.preventDefault() - btn = $(e.target) - text = btn.data("confirm-danger-message") - form = btn.closest("form") - new ConfirmDangerModal(form, text) - - - $document.on 'click', 'button', -> - $(this).blur() - - $('input[type="search"]').each -> - $this = $(this) - $this.attr 'value', $this.val() - return - - $document - .off 'keyup', 'input[type="search"]' - .on 'keyup', 'input[type="search"]' , (e) -> - $this = $(this) - $this.attr 'value', $this.val() - - $sidebarGutterToggle = $('.js-sidebar-toggle') - - $document - .off 'breakpoint:change' - .on 'breakpoint:change', (e, breakpoint) -> - if breakpoint is 'sm' or breakpoint is 'xs' - $gutterIcon = $sidebarGutterToggle.find('i') - if $gutterIcon.hasClass('fa-angle-double-right') - $sidebarGutterToggle.trigger('click') - - fitSidebarForSize = -> - oldBootstrapBreakpoint = bootstrapBreakpoint - bootstrapBreakpoint = bp.getBreakpointSize() - if bootstrapBreakpoint != oldBootstrapBreakpoint - $document.trigger('breakpoint:change', [bootstrapBreakpoint]) - - checkInitialSidebarSize = -> - bootstrapBreakpoint = bp.getBreakpointSize() - if bootstrapBreakpoint is "xs" or "sm" - $document.trigger('breakpoint:change', [bootstrapBreakpoint]) - - $window - .off "resize.app" - .on "resize.app", (e) -> - fitSidebarForSize() - - gl.awardsHandler = new AwardsHandler() - checkInitialSidebarSize() - new Aside() - - # Sidenav pinning - if $window.width() < 1024 and $.cookie('pin_nav') is 'true' - $.cookie('pin_nav', 'false', { path: '/', expires: 365 * 10 }) - $('.page-with-sidebar') - .toggleClass('page-sidebar-collapsed page-sidebar-expanded') - .removeClass('page-sidebar-pinned') - $('.navbar-fixed-top').removeClass('header-pinned-nav') - - $document - .off 'click', '.js-nav-pin' - .on 'click', '.js-nav-pin', (e) -> - e.preventDefault() - - $pinBtn = $(e.currentTarget) - $page = $ '.page-with-sidebar' - $topNav = $ '.navbar-fixed-top' - $tooltip = $ "##{$pinBtn.attr('aria-describedby')}" - doPinNav = not $page.is('.page-sidebar-pinned') - tooltipText = 'Pin navigation' - - $(this).toggleClass 'is-active' - - if doPinNav - $page.addClass('page-sidebar-pinned') - $topNav.addClass('header-pinned-nav') - else - $tooltip.remove() # Remove it immediately when collapsing the sidebar - $page.removeClass('page-sidebar-pinned') - .toggleClass('page-sidebar-collapsed page-sidebar-expanded') - $topNav.removeClass('header-pinned-nav') - .toggleClass('header-collapsed header-expanded') - - # Save settings - $.cookie 'pin_nav', doPinNav, { path: '/', expires: 365 * 10 } - - if $.cookie('pin_nav') is 'true' or doPinNav - tooltipText = 'Unpin navigation' - - # Update tooltip text immediately - $tooltip.find('.tooltip-inner').text(tooltipText) - - # Persist tooltip title - $pinBtn.attr('title', tooltipText).tooltip('fixTitle') diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js new file mode 100644 index 00000000000..7b546e79ee0 --- /dev/null +++ b/app/assets/javascripts/aside.js @@ -0,0 +1,26 @@ +(function() { + this.Aside = (function() { + function Aside() { + $(document).off("click", "a.show-aside"); + $(document).on("click", 'a.show-aside', function(e) { + var btn, icon; + e.preventDefault(); + btn = $(e.currentTarget); + icon = btn.find('i'); + if (icon.hasClass('fa-angle-left')) { + btn.parent().find('section').hide(); + btn.parent().find('aside').fadeIn(); + return icon.removeClass('fa-angle-left').addClass('fa-angle-right'); + } else { + btn.parent().find('aside').hide(); + btn.parent().find('section').fadeIn(); + return icon.removeClass('fa-angle-right').addClass('fa-angle-left'); + } + }); + } + + return Aside; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/aside.js.coffee b/app/assets/javascripts/aside.js.coffee deleted file mode 100644 index 66ab5054326..00000000000 --- a/app/assets/javascripts/aside.js.coffee +++ /dev/null @@ -1,16 +0,0 @@ -class @Aside - constructor: -> - $(document).off "click", "a.show-aside" - $(document).on "click", 'a.show-aside', (e) -> - e.preventDefault() - btn = $(e.currentTarget) - icon = btn.find('i') - - if icon.hasClass('fa-angle-left') - btn.parent().find('section').hide() - btn.parent().find('aside').fadeIn() - icon.removeClass('fa-angle-left').addClass('fa-angle-right') - else - btn.parent().find('aside').hide() - btn.parent().find('section').fadeIn() - icon.removeClass('fa-angle-right').addClass('fa-angle-left') diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js new file mode 100644 index 00000000000..7116512d6b7 --- /dev/null +++ b/app/assets/javascripts/autosave.js @@ -0,0 +1,63 @@ +(function() { + this.Autosave = (function() { + function Autosave(field, key) { + this.field = field; + if (key.join != null) { + key = key.join("/"); + } + this.key = "autosave/" + key; + this.field.data("autosave", this); + this.restore(); + this.field.on("input", (function(_this) { + return function() { + return _this.save(); + }; + })(this)); + } + + Autosave.prototype.restore = function() { + var e, error, text; + if (window.localStorage == null) { + return; + } + try { + text = window.localStorage.getItem(this.key); + } catch (error) { + e = error; + return; + } + if ((text != null ? text.length : void 0) > 0) { + this.field.val(text); + } + return this.field.trigger("input"); + }; + + Autosave.prototype.save = function() { + var text; + if (window.localStorage == null) { + return; + } + text = this.field.val(); + if ((text != null ? text.length : void 0) > 0) { + try { + return window.localStorage.setItem(this.key, text); + } catch (undefined) {} + } else { + return this.reset(); + } + }; + + Autosave.prototype.reset = function() { + if (window.localStorage == null) { + return; + } + try { + return window.localStorage.removeItem(this.key); + } catch (undefined) {} + }; + + return Autosave; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/autosave.js.coffee b/app/assets/javascripts/autosave.js.coffee deleted file mode 100644 index 28f8e103664..00000000000 --- a/app/assets/javascripts/autosave.js.coffee +++ /dev/null @@ -1,39 +0,0 @@ -class @Autosave - constructor: (field, key) -> - @field = field - - key = key.join("/") if key.join? - @key = "autosave/#{key}" - - @field.data "autosave", this - - @restore() - - @field.on "input", => @save() - - restore: -> - return unless window.localStorage? - - try - text = window.localStorage.getItem @key - catch e - return - - @field.val text if text?.length > 0 - @field.trigger "input" - - save: -> - return unless window.localStorage? - - text = @field.val() - if text?.length > 0 - try - window.localStorage.setItem @key, text - else - @reset() - - reset: -> - return unless window.localStorage? - - try - window.localStorage.removeItem @key diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee deleted file mode 100644 index 37d0adaa625..00000000000 --- a/app/assets/javascripts/awards_handler.coffee +++ /dev/null @@ -1,372 +0,0 @@ -class @AwardsHandler - - constructor: -> - - @aliases = gl.emojiAliases() - - $(document) - .off 'click', '.js-add-award' - .on 'click', '.js-add-award', (e) => - e.stopPropagation() - e.preventDefault() - - @showEmojiMenu $(e.currentTarget) - - $('html').on 'click', (e) -> - $target = $ e.target - - unless $target.closest('.emoji-menu-content').length - $('.js-awards-block.current').removeClass 'current' - - unless $target.closest('.emoji-menu').length - if $('.emoji-menu').is(':visible') - $('.js-add-award.is-active').removeClass 'is-active' - $('.emoji-menu').removeClass 'is-visible' - - $(document) - .off 'click', '.js-emoji-btn' - .on 'click', '.js-emoji-btn', (e) => - e.preventDefault() - - $target = $ e.currentTarget - emoji = $target.find('.icon').data 'emoji' - - $target.closest('.js-awards-block').addClass 'current' - @addAward @getVotesBlock(), @getAwardUrl(), emoji - - - showEmojiMenu: ($addBtn) -> - - $menu = $ '.emoji-menu' - - if $addBtn.hasClass 'js-note-emoji' - $addBtn.closest('.note').find('.js-awards-block').addClass 'current' - else - $addBtn.closest('.js-awards-block').addClass 'current' - - if $menu.length - $holder = $addBtn.closest('.js-award-holder') - - if $menu.is '.is-visible' - $addBtn.removeClass 'is-active' - $menu.removeClass 'is-visible' - $('#emoji_search').blur() - else - $addBtn.addClass 'is-active' - @positionMenu($menu, $addBtn) - - $menu.addClass 'is-visible' - $('#emoji_search').focus() - else - $addBtn.addClass 'is-loading is-active' - url = @getAwardMenuUrl() - - @createEmojiMenu url, => - $addBtn.removeClass 'is-loading' - $menu = $('.emoji-menu') - @positionMenu($menu, $addBtn) - @renderFrequentlyUsedBlock() unless @frequentEmojiBlockRendered - - setTimeout => - $menu.addClass 'is-visible' - $('#emoji_search').focus() - @setupSearch() - , 200 - - - createEmojiMenu: (awardMenuUrl, callback) -> - - $.get awardMenuUrl, (response) -> - $('body').append response - callback() - - - positionMenu: ($menu, $addBtn) -> - - position = $addBtn.data('position') - - # The menu could potentially be off-screen or in a hidden overflow element - # So we position the element absolute in the body - css = - top: "#{$addBtn.offset().top + $addBtn.outerHeight()}px" - - if position? and position is 'right' - css.left = "#{($addBtn.offset().left - $menu.outerWidth()) + 20}px" - $menu.addClass 'is-aligned-right' - else - css.left = "#{$addBtn.offset().left}px" - $menu.removeClass 'is-aligned-right' - - $menu.css(css) - - - addAward: (votesBlock, awardUrl, emoji, checkMutuality = true, callback) -> - - emoji = @normilizeEmojiName emoji - - @postEmoji awardUrl, emoji, => - @addAwardToEmojiBar votesBlock, emoji, checkMutuality - callback?() - - $('.emoji-menu').removeClass 'is-visible' - - - addAwardToEmojiBar: (votesBlock, emoji, checkForMutuality = true) -> - - @checkMutuality votesBlock, emoji if checkForMutuality - @addEmojiToFrequentlyUsedList emoji - - emoji = @normilizeEmojiName emoji - $emojiButton = @findEmojiIcon(votesBlock, emoji).parent() - - if $emojiButton.length > 0 - if @isActive $emojiButton - @decrementCounter $emojiButton, emoji - else - counter = $emojiButton.find '.js-counter' - counter.text parseInt(counter.text()) + 1 - $emojiButton.addClass 'active' - @addMeToUserList votesBlock, emoji - @animateEmoji $emojiButton - else - votesBlock.removeClass 'hidden' - @createEmoji votesBlock, emoji - - - getVotesBlock: -> - - currentBlock = $ '.js-awards-block.current' - return if currentBlock.length then currentBlock else $('.js-awards-block').eq 0 - - - getAwardUrl: -> return @getVotesBlock().data 'award-url' - - - checkMutuality: (votesBlock, emoji) -> - - awardUrl = @getAwardUrl() - - if emoji in [ 'thumbsup', 'thumbsdown' ] - mutualVote = if emoji is 'thumbsup' then 'thumbsdown' else 'thumbsup' - $emojiButton = votesBlock.find("[data-emoji=#{mutualVote}]").parent() - isAlreadyVoted = $emojiButton.hasClass 'active' - - if isAlreadyVoted - @showEmojiLoader $emojiButton - @addAward votesBlock, awardUrl, mutualVote, false, -> - $emojiButton.removeClass 'is-loading' - - - showEmojiLoader: ($emojiButton) -> - - $loader = $emojiButton.find '.fa-spinner' - - unless $loader.length - $emojiButton.append '<i class="fa fa-spinner fa-spin award-control-icon award-control-icon-loading"></i>' - - $emojiButton.addClass 'is-loading' - - - isActive: ($emojiButton) -> $emojiButton.hasClass 'active' - - - decrementCounter: ($emojiButton, emoji) -> - - counter = $ '.js-counter', $emojiButton - counterNumber = parseInt counter.text(), 10 - - if counterNumber > 1 - counter.text counterNumber - 1 - @removeMeFromUserList $emojiButton, emoji - else if emoji is 'thumbsup' or emoji is 'thumbsdown' - $emojiButton.tooltip 'destroy' - counter.text '0' - @removeMeFromUserList $emojiButton, emoji - @removeEmoji $emojiButton if $emojiButton.parents('.note').length - else - @removeEmoji $emojiButton - - $emojiButton.removeClass 'active' - - - removeEmoji: ($emojiButton) -> - - $emojiButton.tooltip('destroy') - $emojiButton.remove() - - $votesBlock = @getVotesBlock() - - if $votesBlock.find('.js-emoji-btn').length is 0 - $votesBlock.addClass 'hidden' - - - getAwardTooltip: ($awardBlock) -> - - return $awardBlock.attr('data-original-title') or $awardBlock.attr('data-title') or '' - - - removeMeFromUserList: ($emojiButton, emoji) -> - - awardBlock = $emojiButton - originalTitle = @getAwardTooltip awardBlock - - authors = originalTitle.split ', ' - authors.splice authors.indexOf('me'), 1 - - newAuthors = authors.join ', ' - - awardBlock - .closest '.js-emoji-btn' - .removeData 'original-title' - .attr 'data-original-title', newAuthors - - @resetTooltip awardBlock - - - addMeToUserList: (votesBlock, emoji) -> - - awardBlock = @findEmojiIcon(votesBlock, emoji).parent() - origTitle = @getAwardTooltip awardBlock - users = [] - - if origTitle - users = origTitle.trim().split ', ' - - users.push 'me' - awardBlock.attr 'title', users.join ', ' - - @resetTooltip awardBlock - - - resetTooltip: (award) -> - - award.tooltip 'destroy' - - # 'destroy' call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout. - cb = -> award.tooltip() - setTimeout cb, 200 - - - createEmoji_: (votesBlock, emoji) -> - - emojiCssClass = @resolveNameToCssClass emoji - buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'> - <div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div> - <span class='award-control-text js-counter'>1</span> - </button>" - - $emojiButton = $ buttonHtml - $emojiButton - .insertBefore votesBlock.find '.js-award-holder' - .find '.emoji-icon' - .data 'emoji', emoji - - @animateEmoji $emojiButton - $('.award-control').tooltip() - votesBlock.removeClass 'current' - - - animateEmoji: ($emoji) -> - - className = 'pulse animated' - - $emoji.addClass className - setTimeout (-> $emoji.removeClass className), 321 - - - createEmoji: (votesBlock, emoji) -> - - if $('.emoji-menu').length - return @createEmoji_ votesBlock, emoji - - @createEmojiMenu @getAwardMenuUrl(), => @createEmoji_ votesBlock, emoji - - - getAwardMenuUrl: -> return gon.award_menu_url - - - resolveNameToCssClass: (emoji) -> - - emojiIcon = $ ".emoji-menu-content [data-emoji='#{emoji}']" - - if emojiIcon.length > 0 - unicodeName = emojiIcon.data 'unicode-name' - else - # Find by alias - unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data 'unicode-name' - - return "emoji-#{unicodeName}" - - - postEmoji: (awardUrl, emoji, callback) -> - - $.post awardUrl, { name: emoji }, (data) -> - callback() if data.ok - - - findEmojiIcon: (votesBlock, emoji) -> - - return votesBlock.find ".js-emoji-btn [data-emoji='#{emoji}']" - - - scrollToAwards: -> - - options = scrollTop: $('.awards').offset().top - 110 - $('body, html').animate options, 200 - - - normilizeEmojiName: (emoji) -> return @aliases[emoji] or emoji - - - addEmojiToFrequentlyUsedList: (emoji) -> - - frequentlyUsedEmojis = @getFrequentlyUsedEmojis() - frequentlyUsedEmojis.push emoji - $.cookie 'frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 } - - - getFrequentlyUsedEmojis: -> - - frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') or '').split(',') - return _.compact _.uniq frequentlyUsedEmojis - - - renderFrequentlyUsedBlock: -> - - if $.cookie 'frequently_used_emojis' - frequentlyUsedEmojis = @getFrequentlyUsedEmojis() - - ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>") - - for emoji in frequentlyUsedEmojis - $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul) - - $('.emoji-menu-content') - .prepend(ul) - .prepend($('<h5>').text('Frequently used')) - - @frequentEmojiBlockRendered = true - - - setupSearch: -> - - $('input.emoji-search').on 'keyup', (ev) => - term = $(ev.target).val() - - # Clean previous search results - $('ul.emoji-menu-search, h5.emoji-search').remove() - - if term - # Generate a search result block - h5 = $('<h5>').text('Search results') - found_emojis = @searchEmojis(term).show() - ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis) - $('.emoji-menu-content ul, .emoji-menu-content h5').hide() - $('.emoji-menu-content').append(h5).append(ul) - else - $('.emoji-menu-content').children().show() - - - searchEmojis: (term) -> - - $(".emoji-menu-list:not(.frequent-emojis) [data-emoji*='#{term}']").closest('li').clone() diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js new file mode 100644 index 00000000000..ea683b31f75 --- /dev/null +++ b/app/assets/javascripts/awards_handler.js @@ -0,0 +1,380 @@ +(function() { + this.AwardsHandler = (function() { + function AwardsHandler() { + this.aliases = gl.emojiAliases(); + $(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) { + return function(e) { + e.stopPropagation(); + e.preventDefault(); + return _this.showEmojiMenu($(e.currentTarget)); + }; + })(this)); + $('html').on('click', function(e) { + var $target; + $target = $(e.target); + if (!$target.closest('.emoji-menu-content').length) { + $('.js-awards-block.current').removeClass('current'); + } + if (!$target.closest('.emoji-menu').length) { + if ($('.emoji-menu').is(':visible')) { + $('.js-add-award.is-active').removeClass('is-active'); + return $('.emoji-menu').removeClass('is-visible'); + } + } + }); + $(document).off('click', '.js-emoji-btn').on('click', '.js-emoji-btn', (function(_this) { + return function(e) { + var $target, emoji; + e.preventDefault(); + $target = $(e.currentTarget); + emoji = $target.find('.icon').data('emoji'); + $target.closest('.js-awards-block').addClass('current'); + return _this.addAward(_this.getVotesBlock(), _this.getAwardUrl(), emoji); + }; + })(this)); + } + + AwardsHandler.prototype.showEmojiMenu = function($addBtn) { + var $holder, $menu, url; + $menu = $('.emoji-menu'); + if ($addBtn.hasClass('js-note-emoji')) { + $addBtn.closest('.note').find('.js-awards-block').addClass('current'); + } else { + $addBtn.closest('.js-awards-block').addClass('current'); + } + if ($menu.length) { + $holder = $addBtn.closest('.js-award-holder'); + if ($menu.is('.is-visible')) { + $addBtn.removeClass('is-active'); + $menu.removeClass('is-visible'); + return $('#emoji_search').blur(); + } else { + $addBtn.addClass('is-active'); + this.positionMenu($menu, $addBtn); + $menu.addClass('is-visible'); + return $('#emoji_search').focus(); + } + } else { + $addBtn.addClass('is-loading is-active'); + url = this.getAwardMenuUrl(); + return this.createEmojiMenu(url, (function(_this) { + return function() { + $addBtn.removeClass('is-loading'); + $menu = $('.emoji-menu'); + _this.positionMenu($menu, $addBtn); + if (!_this.frequentEmojiBlockRendered) { + _this.renderFrequentlyUsedBlock(); + } + return setTimeout(function() { + $menu.addClass('is-visible'); + $('#emoji_search').focus(); + return _this.setupSearch(); + }, 200); + }; + })(this)); + } + }; + + AwardsHandler.prototype.createEmojiMenu = function(awardMenuUrl, callback) { + return $.get(awardMenuUrl, function(response) { + $('body').append(response); + return callback(); + }); + }; + + AwardsHandler.prototype.positionMenu = function($menu, $addBtn) { + var css, position; + position = $addBtn.data('position'); + css = { + top: ($addBtn.offset().top + $addBtn.outerHeight()) + "px" + }; + if ((position != null) && position === 'right') { + css.left = (($addBtn.offset().left - $menu.outerWidth()) + 20) + "px"; + $menu.addClass('is-aligned-right'); + } else { + css.left = ($addBtn.offset().left) + "px"; + $menu.removeClass('is-aligned-right'); + } + return $menu.css(css); + }; + + AwardsHandler.prototype.addAward = function(votesBlock, awardUrl, emoji, checkMutuality, callback) { + if (checkMutuality == null) { + checkMutuality = true; + } + emoji = this.normilizeEmojiName(emoji); + this.postEmoji(awardUrl, emoji, (function(_this) { + return function() { + _this.addAwardToEmojiBar(votesBlock, emoji, checkMutuality); + return typeof callback === "function" ? callback() : void 0; + }; + })(this)); + return $('.emoji-menu').removeClass('is-visible'); + }; + + AwardsHandler.prototype.addAwardToEmojiBar = function(votesBlock, emoji, checkForMutuality) { + var $emojiButton, counter; + if (checkForMutuality == null) { + checkForMutuality = true; + } + if (checkForMutuality) { + this.checkMutuality(votesBlock, emoji); + } + this.addEmojiToFrequentlyUsedList(emoji); + emoji = this.normilizeEmojiName(emoji); + $emojiButton = this.findEmojiIcon(votesBlock, emoji).parent(); + if ($emojiButton.length > 0) { + if (this.isActive($emojiButton)) { + return this.decrementCounter($emojiButton, emoji); + } else { + counter = $emojiButton.find('.js-counter'); + counter.text(parseInt(counter.text()) + 1); + $emojiButton.addClass('active'); + this.addMeToUserList(votesBlock, emoji); + return this.animateEmoji($emojiButton); + } + } else { + votesBlock.removeClass('hidden'); + return this.createEmoji(votesBlock, emoji); + } + }; + + AwardsHandler.prototype.getVotesBlock = function() { + var currentBlock; + currentBlock = $('.js-awards-block.current'); + if (currentBlock.length) { + return currentBlock; + } else { + return $('.js-awards-block').eq(0); + } + }; + + AwardsHandler.prototype.getAwardUrl = function() { + return this.getVotesBlock().data('award-url'); + }; + + AwardsHandler.prototype.checkMutuality = function(votesBlock, emoji) { + var $emojiButton, awardUrl, isAlreadyVoted, mutualVote; + awardUrl = this.getAwardUrl(); + if (emoji === 'thumbsup' || emoji === 'thumbsdown') { + mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup'; + $emojiButton = votesBlock.find("[data-emoji=" + mutualVote + "]").parent(); + isAlreadyVoted = $emojiButton.hasClass('active'); + if (isAlreadyVoted) { + this.showEmojiLoader($emojiButton); + return this.addAward(votesBlock, awardUrl, mutualVote, false, function() { + return $emojiButton.removeClass('is-loading'); + }); + } + } + }; + + AwardsHandler.prototype.showEmojiLoader = function($emojiButton) { + var $loader; + $loader = $emojiButton.find('.fa-spinner'); + if (!$loader.length) { + $emojiButton.append('<i class="fa fa-spinner fa-spin award-control-icon award-control-icon-loading"></i>'); + } + return $emojiButton.addClass('is-loading'); + }; + + AwardsHandler.prototype.isActive = function($emojiButton) { + return $emojiButton.hasClass('active'); + }; + + AwardsHandler.prototype.decrementCounter = function($emojiButton, emoji) { + var counter, counterNumber; + counter = $('.js-counter', $emojiButton); + counterNumber = parseInt(counter.text(), 10); + if (counterNumber > 1) { + counter.text(counterNumber - 1); + this.removeMeFromUserList($emojiButton, emoji); + } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') { + $emojiButton.tooltip('destroy'); + counter.text('0'); + this.removeMeFromUserList($emojiButton, emoji); + if ($emojiButton.parents('.note').length) { + this.removeEmoji($emojiButton); + } + } else { + this.removeEmoji($emojiButton); + } + return $emojiButton.removeClass('active'); + }; + + AwardsHandler.prototype.removeEmoji = function($emojiButton) { + var $votesBlock; + $emojiButton.tooltip('destroy'); + $emojiButton.remove(); + $votesBlock = this.getVotesBlock(); + if ($votesBlock.find('.js-emoji-btn').length === 0) { + return $votesBlock.addClass('hidden'); + } + }; + + AwardsHandler.prototype.getAwardTooltip = function($awardBlock) { + return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || ''; + }; + + AwardsHandler.prototype.removeMeFromUserList = function($emojiButton, emoji) { + var authors, awardBlock, newAuthors, originalTitle; + awardBlock = $emojiButton; + originalTitle = this.getAwardTooltip(awardBlock); + authors = originalTitle.split(', '); + authors.splice(authors.indexOf('me'), 1); + newAuthors = authors.join(', '); + awardBlock.closest('.js-emoji-btn').removeData('original-title').attr('data-original-title', newAuthors); + return this.resetTooltip(awardBlock); + }; + + AwardsHandler.prototype.addMeToUserList = function(votesBlock, emoji) { + var awardBlock, origTitle, users; + awardBlock = this.findEmojiIcon(votesBlock, emoji).parent(); + origTitle = this.getAwardTooltip(awardBlock); + users = []; + if (origTitle) { + users = origTitle.trim().split(', '); + } + users.push('me'); + awardBlock.attr('title', users.join(', ')); + return this.resetTooltip(awardBlock); + }; + + AwardsHandler.prototype.resetTooltip = function(award) { + var cb; + award.tooltip('destroy'); + cb = function() { + return award.tooltip(); + }; + return setTimeout(cb, 200); + }; + + AwardsHandler.prototype.createEmoji_ = function(votesBlock, emoji) { + var $emojiButton, buttonHtml, emojiCssClass; + emojiCssClass = this.resolveNameToCssClass(emoji); + buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'> <div class='icon emoji-icon " + emojiCssClass + "' data-emoji='" + emoji + "'></div> <span class='award-control-text js-counter'>1</span> </button>"; + $emojiButton = $(buttonHtml); + $emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('emoji', emoji); + this.animateEmoji($emojiButton); + $('.award-control').tooltip(); + return votesBlock.removeClass('current'); + }; + + AwardsHandler.prototype.animateEmoji = function($emoji) { + var className; + className = 'pulse animated'; + $emoji.addClass(className); + return setTimeout((function() { + return $emoji.removeClass(className); + }), 321); + }; + + AwardsHandler.prototype.createEmoji = function(votesBlock, emoji) { + if ($('.emoji-menu').length) { + return this.createEmoji_(votesBlock, emoji); + } + return this.createEmojiMenu(this.getAwardMenuUrl(), (function(_this) { + return function() { + return _this.createEmoji_(votesBlock, emoji); + }; + })(this)); + }; + + AwardsHandler.prototype.getAwardMenuUrl = function() { + return gon.award_menu_url; + }; + + AwardsHandler.prototype.resolveNameToCssClass = function(emoji) { + var emojiIcon, unicodeName; + emojiIcon = $(".emoji-menu-content [data-emoji='" + emoji + "']"); + if (emojiIcon.length > 0) { + unicodeName = emojiIcon.data('unicode-name'); + } else { + unicodeName = $(".emoji-menu-content [data-aliases*=':" + emoji + ":']").data('unicode-name'); + } + return "emoji-" + unicodeName; + }; + + AwardsHandler.prototype.postEmoji = function(awardUrl, emoji, callback) { + return $.post(awardUrl, { + name: emoji + }, function(data) { + if (data.ok) { + return callback(); + } + }); + }; + + AwardsHandler.prototype.findEmojiIcon = function(votesBlock, emoji) { + return votesBlock.find(".js-emoji-btn [data-emoji='" + emoji + "']"); + }; + + AwardsHandler.prototype.scrollToAwards = function() { + var options; + options = { + scrollTop: $('.awards').offset().top - 110 + }; + return $('body, html').animate(options, 200); + }; + + AwardsHandler.prototype.normilizeEmojiName = function(emoji) { + return this.aliases[emoji] || emoji; + }; + + AwardsHandler.prototype.addEmojiToFrequentlyUsedList = function(emoji) { + var frequentlyUsedEmojis; + frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); + frequentlyUsedEmojis.push(emoji); + return $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { + expires: 365 + }); + }; + + AwardsHandler.prototype.getFrequentlyUsedEmojis = function() { + var frequentlyUsedEmojis; + frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(','); + return _.compact(_.uniq(frequentlyUsedEmojis)); + }; + + AwardsHandler.prototype.renderFrequentlyUsedBlock = function() { + var emoji, frequentlyUsedEmojis, i, len, ul; + if ($.cookie('frequently_used_emojis')) { + frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); + ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>"); + for (i = 0, len = frequentlyUsedEmojis.length; i < len; i++) { + emoji = frequentlyUsedEmojis[i]; + $(".emoji-menu-content [data-emoji='" + emoji + "']").closest('li').clone().appendTo(ul); + } + $('.emoji-menu-content').prepend(ul).prepend($('<h5>').text('Frequently used')); + } + return this.frequentEmojiBlockRendered = true; + }; + + AwardsHandler.prototype.setupSearch = function() { + return $('input.emoji-search').on('keyup', (function(_this) { + return function(ev) { + var found_emojis, h5, term, ul; + term = $(ev.target).val(); + $('ul.emoji-menu-search, h5.emoji-search').remove(); + if (term) { + h5 = $('<h5>').text('Search results'); + found_emojis = _this.searchEmojis(term).show(); + ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis); + $('.emoji-menu-content ul, .emoji-menu-content h5').hide(); + return $('.emoji-menu-content').append(h5).append(ul); + } else { + return $('.emoji-menu-content').children().show(); + } + }; + })(this)); + }; + + AwardsHandler.prototype.searchEmojis = function(term) { + return $(".emoji-menu-list:not(.frequent-emojis) [data-emoji*='" + term + "']").closest('li').clone(); + }; + + return AwardsHandler; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js new file mode 100644 index 00000000000..f977a1e8a7b --- /dev/null +++ b/app/assets/javascripts/behaviors/autosize.js @@ -0,0 +1,30 @@ + +/*= require jquery.ba-resize */ + + +/*= require autosize */ + +(function() { + $(function() { + var $fields; + $fields = $('.js-autosize'); + $fields.on('autosize:resized', function() { + var $field; + $field = $(this); + return $field.data('height', $field.outerHeight()); + }); + $fields.on('resize.autosize', function() { + var $field; + $field = $(this); + if ($field.data('height') !== $field.outerHeight()) { + $field.data('height', $field.outerHeight()); + autosize.destroy($field); + return $field.css('max-height', window.outerHeight); + } + }); + autosize($fields); + autosize.update($fields); + return $fields.css('resize', 'vertical'); + }); + +}).call(this); diff --git a/app/assets/javascripts/behaviors/autosize.js.coffee b/app/assets/javascripts/behaviors/autosize.js.coffee deleted file mode 100644 index a072fe48a98..00000000000 --- a/app/assets/javascripts/behaviors/autosize.js.coffee +++ /dev/null @@ -1,22 +0,0 @@ -#= require jquery.ba-resize -#= require autosize - -$ -> - $fields = $('.js-autosize') - - $fields.on 'autosize:resized', -> - $field = $(@) - $field.data('height', $field.outerHeight()) - - $fields.on 'resize.autosize', -> - $field = $(@) - - if $field.data('height') != $field.outerHeight() - $field.data('height', $field.outerHeight()) - autosize.destroy($field) - $field.css('max-height', window.outerHeight) - - autosize($fields) - autosize.update($fields) - - $fields.css('resize', 'vertical') diff --git a/app/assets/javascripts/behaviors/details_behavior.coffee b/app/assets/javascripts/behaviors/details_behavior.coffee deleted file mode 100644 index decab3e1bed..00000000000 --- a/app/assets/javascripts/behaviors/details_behavior.coffee +++ /dev/null @@ -1,15 +0,0 @@ -$ -> - $("body").on "click", ".js-details-target", -> - container = $(@).closest(".js-details-container") - container.toggleClass("open") - - # Show details content. Hides link after click. - # - # %div - # %a.js-details-expand - # %div.js-details-content - # - $("body").on "click", ".js-details-expand", (e) -> - $(@).next('.js-details-content').removeClass("hide") - $(@).hide() - e.preventDefault() diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js new file mode 100644 index 00000000000..3631d1b74ac --- /dev/null +++ b/app/assets/javascripts/behaviors/details_behavior.js @@ -0,0 +1,15 @@ +(function() { + $(function() { + $("body").on("click", ".js-details-target", function() { + var container; + container = $(this).closest(".js-details-container"); + return container.toggleClass("open"); + }); + return $("body").on("click", ".js-details-expand", function(e) { + $(this).next('.js-details-content').removeClass("hide"); + $(this).hide(); + return e.preventDefault(); + }); + }); + +}).call(this); diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js new file mode 100644 index 00000000000..3527d0a95fc --- /dev/null +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -0,0 +1,58 @@ + +/*= require extensions/jquery */ + +(function() { + var isMac, keyCodeIs; + + isMac = function() { + return navigator.userAgent.match(/Macintosh/); + }; + + keyCodeIs = function(e, keyCode) { + if ((e.originalEvent && e.originalEvent.repeat) || e.repeat) { + return false; + } + return e.keyCode === keyCode; + }; + + $(document).on('keydown.quick_submit', '.js-quick-submit', function(e) { + var $form, $submit_button; + if (!keyCodeIs(e, 13)) { + return; + } + if (!((e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey) || (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey))) { + return; + } + e.preventDefault(); + $form = $(e.target).closest('form'); + $submit_button = $form.find('input[type=submit], button[type=submit]'); + if ($submit_button.attr('disabled')) { + return; + } + $submit_button.disable(); + return $form.submit(); + }); + + $(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', function(e) { + var $this, title; + if (!keyCodeIs(e, 9)) { + return; + } + if (isMac()) { + title = "You can also press ⌘-Enter"; + } else { + title = "You can also press Ctrl-Enter"; + } + $this = $(this); + return $this.tooltip({ + container: 'body', + html: 'true', + placement: 'auto top', + title: title, + trigger: 'manual' + }).tooltip('show').one('blur', function() { + return $this.tooltip('hide'); + }); + }); + +}).call(this); diff --git a/app/assets/javascripts/behaviors/quick_submit.js.coffee b/app/assets/javascripts/behaviors/quick_submit.js.coffee deleted file mode 100644 index 3cb96bacaa7..00000000000 --- a/app/assets/javascripts/behaviors/quick_submit.js.coffee +++ /dev/null @@ -1,56 +0,0 @@ -# Quick Submit behavior -# -# When a child field of a form with a `js-quick-submit` class receives a -# "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form -# is submitted. -# -#= require extensions/jquery -# -# ### Example Markup -# -# <form action="/foo" class="js-quick-submit"> -# <input type="text" /> -# <textarea></textarea> -# <input type="submit" value="Submit" /> -# </form> -# -isMac = -> - navigator.userAgent.match(/Macintosh/) - -keyCodeIs = (e, keyCode) -> - return false if (e.originalEvent && e.originalEvent.repeat) || e.repeat - return e.keyCode == keyCode - -$(document).on 'keydown.quick_submit', '.js-quick-submit', (e) -> - return unless keyCodeIs(e, 13) # Enter - - return unless (e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey) || (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) - - e.preventDefault() - - $form = $(e.target).closest('form') - $submit_button = $form.find('input[type=submit], button[type=submit]') - - return if $submit_button.attr('disabled') - - $submit_button.disable() - $form.submit() - -# If the user tabs to a submit button on a `js-quick-submit` form, display a -# tooltip to let them know they could've used the hotkey -$(document).on 'keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', (e) -> - return unless keyCodeIs(e, 9) # Tab - - if isMac() - title = "You can also press ⌘-Enter" - else - title = "You can also press Ctrl-Enter" - - $this = $(@) - $this.tooltip( - container: 'body' - html: 'true' - placement: 'auto top' - title: title - trigger: 'manual' - ).tooltip('show').one('blur', -> $this.tooltip('hide')) diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js new file mode 100644 index 00000000000..db0b36b24e9 --- /dev/null +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -0,0 +1,45 @@ + +/*= require extensions/jquery */ + +(function() { + $.fn.requiresInput = function() { + var $button, $form, fieldSelector, requireInput, required; + $form = $(this); + $button = $('button[type=submit], input[type=submit]', $form); + required = '[required=required]'; + fieldSelector = "input" + required + ", select" + required + ", textarea" + required; + requireInput = function() { + var values; + values = _.map($(fieldSelector, $form), function(field) { + return field.value; + }); + if (values.length && _.any(values, _.isEmpty)) { + return $button.disable(); + } else { + return $button.enable(); + } + }; + requireInput(); + return $form.on('change input', fieldSelector, requireInput); + }; + + $(function() { + var $form, hideOrShowHelpBlock; + $form = $('form.js-requires-input'); + $form.requiresInput(); + hideOrShowHelpBlock = function(form) { + var selected; + selected = $('.js-select-namespace option:selected'); + if (selected.length && selected.data('options-parent') === 'groups') { + return form.find('.help-block').hide(); + } else if (selected.length) { + return form.find('.help-block').show(); + } + }; + hideOrShowHelpBlock($form); + return $('.select2.js-select-namespace').change(function() { + return hideOrShowHelpBlock($form); + }); + }); + +}).call(this); diff --git a/app/assets/javascripts/behaviors/requires_input.js.coffee b/app/assets/javascripts/behaviors/requires_input.js.coffee deleted file mode 100644 index 0faa570ce13..00000000000 --- a/app/assets/javascripts/behaviors/requires_input.js.coffee +++ /dev/null @@ -1,52 +0,0 @@ -# Requires Input behavior -# -# When called on a form with input fields with the `required` attribute, the -# form's submit button will be disabled until all required fields have values. -# -#= require extensions/jquery -# -# ### Example Markup -# -# <form class="js-requires-input"> -# <input type="text" required="required"> -# <input type="submit" value="Submit"> -# </form> -# -$.fn.requiresInput = -> - $form = $(this) - $button = $('button[type=submit], input[type=submit]', $form) - - required = '[required=required]' - fieldSelector = "input#{required}, select#{required}, textarea#{required}" - - requireInput = -> - # Collect the input values of *all* required fields - values = _.map $(fieldSelector, $form), (field) -> field.value - - # Disable the button if any required fields are empty - if values.length && _.any(values, _.isEmpty) - $button.disable() - else - $button.enable() - - # Set initial button state - requireInput() - - $form.on 'change input', fieldSelector, requireInput - -$ -> - $form = $('form.js-requires-input') - $form.requiresInput() - - # Hide or Show the help block when creating a new project - # based on the option selected - hideOrShowHelpBlock = (form) -> - selected = $('.js-select-namespace option:selected') - if selected.length and selected.data('options-parent') is 'groups' - return form.find('.help-block').hide() - else if selected.length - form.find('.help-block').show() - - hideOrShowHelpBlock($form) - - $('.select2.js-select-namespace').change -> hideOrShowHelpBlock($form) diff --git a/app/assets/javascripts/behaviors/toggler_behavior.coffee b/app/assets/javascripts/behaviors/toggler_behavior.coffee deleted file mode 100644 index 177b6918270..00000000000 --- a/app/assets/javascripts/behaviors/toggler_behavior.coffee +++ /dev/null @@ -1,14 +0,0 @@ -$ -> - # Toggle button. Show/hide content inside parent container. - # Button does not change visibility. If button has icon - it changes chevron style. - # - # %div.js-toggle-container - # %a.js-toggle-button - # %div.js-toggle-content - # - $("body").on "click", ".js-toggle-button", (e) -> - $(@).find('i'). - toggleClass('fa fa-chevron-down'). - toggleClass('fa fa-chevron-up') - $(@).closest(".js-toggle-container").find(".js-toggle-content").toggle() - e.preventDefault() diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js new file mode 100644 index 00000000000..1b7b63489ea --- /dev/null +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -0,0 +1,10 @@ +(function() { + $(function() { + return $("body").on("click", ".js-toggle-button", function(e) { + $(this).find('i').toggleClass('fa fa-chevron-down').toggleClass('fa fa-chevron-up'); + $(this).closest(".js-toggle-container").find(".js-toggle-content").toggle(); + return e.preventDefault(); + }); + }); + +}).call(this); diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js b/app/assets/javascripts/blob/blob_ci_yaml.js new file mode 100644 index 00000000000..68758574967 --- /dev/null +++ b/app/assets/javascripts/blob/blob_ci_yaml.js @@ -0,0 +1,46 @@ + +/*= require blob/template_selector */ + +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.BlobCiYamlSelector = (function(superClass) { + extend(BlobCiYamlSelector, superClass); + + function BlobCiYamlSelector() { + return BlobCiYamlSelector.__super__.constructor.apply(this, arguments); + } + + BlobCiYamlSelector.prototype.requestFile = function(query) { + return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this)); + }; + + return BlobCiYamlSelector; + + })(TemplateSelector); + + this.BlobCiYamlSelectors = (function() { + function BlobCiYamlSelectors(opts) { + var ref; + this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-gitlab-ci-yml-selector'), this.editor = opts.editor; + this.$dropdowns.each((function(_this) { + return function(i, dropdown) { + var $dropdown; + $dropdown = $(dropdown); + return new BlobCiYamlSelector({ + pattern: /(.gitlab-ci.yml)/, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'), + dropdown: $dropdown, + editor: _this.editor + }); + }; + })(this)); + } + + return BlobCiYamlSelectors; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.coffee b/app/assets/javascripts/blob/blob_ci_yaml.js.coffee deleted file mode 100644 index d9a03d05529..00000000000 --- a/app/assets/javascripts/blob/blob_ci_yaml.js.coffee +++ /dev/null @@ -1,23 +0,0 @@ -#= require blob/template_selector - -class @BlobCiYamlSelector extends TemplateSelector - requestFile: (query) -> - Api.gitlabCiYml query.name, @requestFileSuccess.bind(@) - -class @BlobCiYamlSelectors - constructor: (opts) -> - { - @$dropdowns = $('.js-gitlab-ci-yml-selector') - @editor - } = opts - - @$dropdowns.each (i, dropdown) => - $dropdown = $(dropdown) - - new BlobCiYamlSelector( - pattern: /(.gitlab-ci.yml)/, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'), - dropdown: $dropdown, - editor: @editor - ) diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js new file mode 100644 index 00000000000..f4044f22db2 --- /dev/null +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -0,0 +1,62 @@ +(function() { + this.BlobFileDropzone = (function() { + function BlobFileDropzone(form, method) { + var dropzone, form_dropzone, submitButton; + form_dropzone = form.find('.dropzone'); + Dropzone.autoDiscover = false; + dropzone = form_dropzone.dropzone({ + autoDiscover: false, + autoProcessQueue: false, + url: form.attr('action'), + method: method, + clickable: true, + uploadMultiple: false, + paramName: "file", + maxFilesize: gon.max_file_size || 10, + parallelUploads: 1, + maxFiles: 1, + addRemoveLinks: true, + previewsContainer: '.dropzone-previews', + headers: { + "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + }, + init: function() { + this.on('addedfile', function(file) { + $('.dropzone-alerts').html('').hide(); + }); + this.on('success', function(header, response) { + window.location.href = response.filePath; + }); + this.on('maxfilesexceeded', function(file) { + this.removeFile(file); + }); + return this.on('sending', function(file, xhr, formData) { + formData.append('target_branch', form.find('.js-target-branch').val()); + formData.append('create_merge_request', form.find('.js-create-merge-request').val()); + formData.append('commit_message', form.find('.js-commit-message').val()); + }); + }, + error: function(file, errorMessage) { + var stripped; + stripped = $("<div/>").html(errorMessage).text(); + $('.dropzone-alerts').html('Error uploading file: \"' + stripped + '\"').show(); + this.removeFile(file); + } + }); + submitButton = form.find('#submit-all')[0]; + submitButton.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + if (dropzone[0].dropzone.getQueuedFiles().length === 0) { + alert("Please select a file"); + } + dropzone[0].dropzone.processQueue(); + return false; + }); + } + + return BlobFileDropzone; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee deleted file mode 100644 index 9df932817f6..00000000000 --- a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee +++ /dev/null @@ -1,57 +0,0 @@ -class @BlobFileDropzone - constructor: (form, method) -> - form_dropzone = form.find('.dropzone') - Dropzone.autoDiscover = false - dropzone = form_dropzone.dropzone( - autoDiscover: false - autoProcessQueue: false - url: form.attr('action') - # Rails uses a hidden input field for PUT - # http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails - method: method - clickable: true - uploadMultiple: false - paramName: "file" - maxFilesize: gon.max_file_size or 10 - parallelUploads: 1 - maxFiles: 1 - addRemoveLinks: true - previewsContainer: '.dropzone-previews' - headers: - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") - - init: -> - this.on 'addedfile', (file) -> - $('.dropzone-alerts').html('').hide() - - return - - this.on 'success', (header, response) -> - window.location.href = response.filePath - return - - this.on 'maxfilesexceeded', (file) -> - @removeFile file - return - - this.on 'sending', (file, xhr, formData) -> - formData.append('target_branch', form.find('.js-target-branch').val()) - formData.append('create_merge_request', form.find('.js-create-merge-request').val()) - formData.append('commit_message', form.find('.js-commit-message').val()) - return - - # Override behavior of adding error underneath preview - error: (file, errorMessage) -> - stripped = $("<div/>").html(errorMessage).text(); - $('.dropzone-alerts').html('Error uploading file: \"' + stripped + '\"').show() - @removeFile file - return - ) - - submitButton = form.find('#submit-all')[0] - submitButton.addEventListener 'click', (e) -> - e.preventDefault() - e.stopPropagation() - alert "Please select a file" if dropzone[0].dropzone.getQueuedFiles().length == 0 - dropzone[0].dropzone.processQueue() - return false diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js new file mode 100644 index 00000000000..54a09e919f8 --- /dev/null +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js @@ -0,0 +1,23 @@ + +/*= require blob/template_selector */ + +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.BlobGitignoreSelector = (function(superClass) { + extend(BlobGitignoreSelector, superClass); + + function BlobGitignoreSelector() { + return BlobGitignoreSelector.__super__.constructor.apply(this, arguments); + } + + BlobGitignoreSelector.prototype.requestFile = function(query) { + return Api.gitignoreText(query.name, this.requestFileSuccess.bind(this)); + }; + + return BlobGitignoreSelector; + + })(TemplateSelector); + +}).call(this); diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee deleted file mode 100644 index 8d0e3f363d1..00000000000 --- a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee +++ /dev/null @@ -1,5 +0,0 @@ -#= require blob/template_selector - -class @BlobGitignoreSelector extends TemplateSelector - requestFile: (query) -> - Api.gitignoreText query.name, @requestFileSuccess.bind(@) diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js new file mode 100644 index 00000000000..4e9500428b2 --- /dev/null +++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js @@ -0,0 +1,25 @@ +(function() { + this.BlobGitignoreSelectors = (function() { + function BlobGitignoreSelectors(opts) { + var ref; + this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-gitignore-selector'), this.editor = opts.editor; + this.$dropdowns.each((function(_this) { + return function(i, dropdown) { + var $dropdown; + $dropdown = $(dropdown); + return new BlobGitignoreSelector({ + pattern: /(.gitignore)/, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-gitignore-selector-wrap'), + dropdown: $dropdown, + editor: _this.editor + }); + }; + })(this)); + } + + return BlobGitignoreSelectors; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee deleted file mode 100644 index a719ba25122..00000000000 --- a/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee +++ /dev/null @@ -1,17 +0,0 @@ -class @BlobGitignoreSelectors - constructor: (opts) -> - { - @$dropdowns = $('.js-gitignore-selector') - @editor - } = opts - - @$dropdowns.each (i, dropdown) => - $dropdown = $(dropdown) - - new BlobGitignoreSelector( - pattern: /(.gitignore)/, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-gitignore-selector-wrap'), - dropdown: $dropdown, - editor: @editor - ) diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js new file mode 100644 index 00000000000..9a8ef08f4e5 --- /dev/null +++ b/app/assets/javascripts/blob/blob_license_selector.js @@ -0,0 +1,28 @@ + +/*= require blob/template_selector */ + +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.BlobLicenseSelector = (function(superClass) { + extend(BlobLicenseSelector, superClass); + + function BlobLicenseSelector() { + return BlobLicenseSelector.__super__.constructor.apply(this, arguments); + } + + BlobLicenseSelector.prototype.requestFile = function(query) { + var data; + data = { + project: this.dropdown.data('project'), + fullname: this.dropdown.data('fullname') + }; + return Api.licenseText(query.id, data, this.requestFileSuccess.bind(this)); + }; + + return BlobLicenseSelector; + + })(TemplateSelector); + +}).call(this); diff --git a/app/assets/javascripts/blob/blob_license_selector.js.coffee b/app/assets/javascripts/blob/blob_license_selector.js.coffee deleted file mode 100644 index a3cc8dd844c..00000000000 --- a/app/assets/javascripts/blob/blob_license_selector.js.coffee +++ /dev/null @@ -1,9 +0,0 @@ -#= require blob/template_selector - -class @BlobLicenseSelector extends TemplateSelector - requestFile: (query) -> - data = - project: @dropdown.data('project') - fullname: @dropdown.data('fullname') - - Api.licenseText query.id, data, @requestFileSuccess.bind(@) diff --git a/app/assets/javascripts/blob/blob_license_selectors.js b/app/assets/javascripts/blob/blob_license_selectors.js new file mode 100644 index 00000000000..39237705e8d --- /dev/null +++ b/app/assets/javascripts/blob/blob_license_selectors.js @@ -0,0 +1,25 @@ +(function() { + this.BlobLicenseSelectors = (function() { + function BlobLicenseSelectors(opts) { + var ref; + this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-license-selector'), this.editor = opts.editor; + this.$dropdowns.each((function(_this) { + return function(i, dropdown) { + var $dropdown; + $dropdown = $(dropdown); + return new BlobLicenseSelector({ + pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-license-selector-wrap'), + dropdown: $dropdown, + editor: _this.editor + }); + }; + })(this)); + } + + return BlobLicenseSelectors; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.coffee b/app/assets/javascripts/blob/blob_license_selectors.js.coffee deleted file mode 100644 index 68438733108..00000000000 --- a/app/assets/javascripts/blob/blob_license_selectors.js.coffee +++ /dev/null @@ -1,17 +0,0 @@ -class @BlobLicenseSelectors - constructor: (opts) -> - { - @$dropdowns = $('.js-license-selector') - @editor - } = opts - - @$dropdowns.each (i, dropdown) => - $dropdown = $(dropdown) - - new BlobLicenseSelector( - pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-license-selector-wrap'), - dropdown: $dropdown, - editor: @editor - ) diff --git a/app/assets/javascripts/blob/edit_blob.js b/app/assets/javascripts/blob/edit_blob.js new file mode 100644 index 00000000000..649c79daee8 --- /dev/null +++ b/app/assets/javascripts/blob/edit_blob.js @@ -0,0 +1,66 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.EditBlob = (function() { + function EditBlob(assets_path, ace_mode) { + if (ace_mode == null) { + ace_mode = null; + } + this.editModeLinkClickHandler = bind(this.editModeLinkClickHandler, this); + ace.config.set("modePath", assets_path + "/ace"); + ace.config.loadModule("ace/ext/searchbox"); + this.editor = ace.edit("editor"); + this.editor.focus(); + if (ace_mode) { + this.editor.getSession().setMode("ace/mode/" + ace_mode); + } + $('form').submit((function(_this) { + return function() { + return $("#file-content").val(_this.editor.getValue()); + }; + })(this)); + this.initModePanesAndLinks(); + new BlobLicenseSelectors({ + editor: this.editor + }); + new BlobGitignoreSelectors({ + editor: this.editor + }); + new BlobCiYamlSelectors({ + editor: this.editor + }); + } + + EditBlob.prototype.initModePanesAndLinks = function() { + this.$editModePanes = $(".js-edit-mode-pane"); + this.$editModeLinks = $(".js-edit-mode a"); + return this.$editModeLinks.click(this.editModeLinkClickHandler); + }; + + EditBlob.prototype.editModeLinkClickHandler = function(event) { + var currentLink, currentPane, paneId; + event.preventDefault(); + currentLink = $(event.target); + paneId = currentLink.attr("href"); + currentPane = this.$editModePanes.filter(paneId); + this.$editModeLinks.parent().removeClass("active hover"); + currentLink.parent().addClass("active hover"); + this.$editModePanes.hide(); + currentPane.fadeIn(200); + if (paneId === "#preview") { + return $.post(currentLink.data("preview-url"), { + content: this.editor.getValue() + }, function(response) { + currentPane.empty().append(response); + return currentPane.syntaxHighlight(); + }); + } else { + return this.editor.focus(); + } + }; + + return EditBlob; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee deleted file mode 100644 index 19e584519d7..00000000000 --- a/app/assets/javascripts/blob/edit_blob.js.coffee +++ /dev/null @@ -1,42 +0,0 @@ -class @EditBlob - constructor: (assets_path, ace_mode = null) -> - ace.config.set "modePath", "#{assets_path}/ace" - ace.config.loadModule "ace/ext/searchbox" - @editor = ace.edit("editor") - @editor.focus() - @editor.getSession().setMode "ace/mode/#{ace_mode}" if ace_mode - - # Before a form submission, move the content from the Ace editor into the - # submitted textarea - $('form').submit => - $("#file-content").val(@editor.getValue()) - - @initModePanesAndLinks() - - new BlobLicenseSelectors { @editor } - new BlobGitignoreSelectors { @editor } - new BlobCiYamlSelectors { @editor } - - initModePanesAndLinks: -> - @$editModePanes = $(".js-edit-mode-pane") - @$editModeLinks = $(".js-edit-mode a") - @$editModeLinks.click @editModeLinkClickHandler - - editModeLinkClickHandler: (event) => - event.preventDefault() - currentLink = $(event.target) - paneId = currentLink.attr("href") - currentPane = @$editModePanes.filter(paneId) - @$editModeLinks.parent().removeClass "active hover" - currentLink.parent().addClass "active hover" - @$editModePanes.hide() - currentPane.fadeIn 200 - if paneId is "#preview" - $.post currentLink.data("preview-url"), - content: @editor.getValue() - , (response) -> - currentPane.empty().append response - currentPane.syntaxHighlight() - - else - @editor.focus() diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js new file mode 100644 index 00000000000..2cf0a6631b8 --- /dev/null +++ b/app/assets/javascripts/blob/template_selector.js @@ -0,0 +1,74 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.TemplateSelector = (function() { + function TemplateSelector(opts) { + var ref; + if (opts == null) { + opts = {}; + } + this.onClick = bind(this.onClick, this); + this.dropdown = opts.dropdown, this.data = opts.data, this.pattern = opts.pattern, this.wrapper = opts.wrapper, this.editor = opts.editor, this.fileEndpoint = opts.fileEndpoint, this.$input = (ref = opts.$input) != null ? ref : $('#file_name'); + this.buildDropdown(); + this.bindEvents(); + this.onFilenameUpdate(); + } + + TemplateSelector.prototype.buildDropdown = function() { + return this.dropdown.glDropdown({ + data: this.data, + filterable: true, + selectable: true, + toggleLabel: this.toggleLabel, + search: { + fields: ['name'] + }, + clicked: this.onClick, + text: function(item) { + return item.name; + } + }); + }; + + TemplateSelector.prototype.bindEvents = function() { + return this.$input.on('keyup blur', (function(_this) { + return function(e) { + return _this.onFilenameUpdate(); + }; + })(this)); + }; + + TemplateSelector.prototype.toggleLabel = function(item) { + return item.name; + }; + + TemplateSelector.prototype.onFilenameUpdate = function() { + var filenameMatches; + if (!this.$input.length) { + return; + } + filenameMatches = this.pattern.test(this.$input.val().trim()); + if (!filenameMatches) { + this.wrapper.addClass('hidden'); + return; + } + return this.wrapper.removeClass('hidden'); + }; + + TemplateSelector.prototype.onClick = function(item, el, e) { + e.preventDefault(); + return this.requestFile(item); + }; + + TemplateSelector.prototype.requestFile = function(item) {}; + + TemplateSelector.prototype.requestFileSuccess = function(file) { + this.editor.setValue(file.content, 1); + return this.editor.focus(); + }; + + return TemplateSelector; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/blob/template_selector.js.coffee b/app/assets/javascripts/blob/template_selector.js.coffee deleted file mode 100644 index 40c9169beac..00000000000 --- a/app/assets/javascripts/blob/template_selector.js.coffee +++ /dev/null @@ -1,60 +0,0 @@ -class @TemplateSelector - constructor: (opts = {}) -> - { - @dropdown, - @data, - @pattern, - @wrapper, - @editor, - @fileEndpoint, - @$input = $('#file_name') - } = opts - - @buildDropdown() - @bindEvents() - @onFilenameUpdate() - - buildDropdown: -> - @dropdown.glDropdown( - data: @data, - filterable: true, - selectable: true, - toggleLabel: @toggleLabel, - search: - fields: ['name'] - clicked: @onClick - text: (item) -> - item.name - ) - - bindEvents: -> - @$input.on('keyup blur', (e) => - @onFilenameUpdate() - ) - - toggleLabel: (item) -> - item.name - - onFilenameUpdate: -> - return unless @$input.length - - filenameMatches = @pattern.test(@$input.val().trim()) - - if not filenameMatches - @wrapper.addClass('hidden') - return - - @wrapper.removeClass('hidden') - - onClick: (item, el, e) => - e.preventDefault() - @requestFile(item) - - requestFile: (item) -> - # To be implemented on the extending class - # e.g. - # Api.gitignoreText item.name, @requestFileSuccess.bind(@) - - requestFileSuccess: (file) -> - @editor.setValue(file.content, 1) - @editor.focus() diff --git a/app/assets/javascripts/breakpoints.coffee b/app/assets/javascripts/breakpoints.coffee deleted file mode 100644 index 5457430f921..00000000000 --- a/app/assets/javascripts/breakpoints.coffee +++ /dev/null @@ -1,37 +0,0 @@ -class @Breakpoints - instance = null; - - class BreakpointInstance - BREAKPOINTS = ["xs", "sm", "md", "lg"] - - constructor: -> - @setup() - - setup: -> - allDeviceSelector = BREAKPOINTS.map (breakpoint) -> - ".device-#{breakpoint}" - return if $(allDeviceSelector.join(",")).length - - # Create all the elements - els = $.map BREAKPOINTS, (breakpoint) -> - "<div class='device-#{breakpoint} visible-#{breakpoint}'></div>" - $("body").append els.join('') - - visibleDevice: -> - allDeviceSelector = BREAKPOINTS.map (breakpoint) -> - ".device-#{breakpoint}" - $(allDeviceSelector.join(",")).filter(":visible") - - getBreakpointSize: -> - $visibleDevice = @visibleDevice - # the page refreshed via turbolinks - if not $visibleDevice().length - @setup() - $visibleDevice = @visibleDevice() - return $visibleDevice.attr("class").split("visible-")[1] - - @get: -> - return instance ?= new BreakpointInstance - -$ => - @bp = Breakpoints.get() diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js new file mode 100644 index 00000000000..1e0148e5798 --- /dev/null +++ b/app/assets/javascripts/breakpoints.js @@ -0,0 +1,68 @@ +(function() { + this.Breakpoints = (function() { + var BreakpointInstance, instance; + + function Breakpoints() {} + + instance = null; + + BreakpointInstance = (function() { + var BREAKPOINTS; + + BREAKPOINTS = ["xs", "sm", "md", "lg"]; + + function BreakpointInstance() { + this.setup(); + } + + BreakpointInstance.prototype.setup = function() { + var allDeviceSelector, els; + allDeviceSelector = BREAKPOINTS.map(function(breakpoint) { + return ".device-" + breakpoint; + }); + if ($(allDeviceSelector.join(",")).length) { + return; + } + els = $.map(BREAKPOINTS, function(breakpoint) { + return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>"; + }); + return $("body").append(els.join('')); + }; + + BreakpointInstance.prototype.visibleDevice = function() { + var allDeviceSelector; + allDeviceSelector = BREAKPOINTS.map(function(breakpoint) { + return ".device-" + breakpoint; + }); + return $(allDeviceSelector.join(",")).filter(":visible"); + }; + + BreakpointInstance.prototype.getBreakpointSize = function() { + var $visibleDevice; + $visibleDevice = this.visibleDevice; + if (!$visibleDevice().length) { + this.setup(); + } + $visibleDevice = this.visibleDevice(); + return $visibleDevice.attr("class").split("visible-")[1]; + }; + + return BreakpointInstance; + + })(); + + Breakpoints.get = function() { + return instance != null ? instance : instance = new BreakpointInstance; + }; + + return Breakpoints; + + })(); + + $((function(_this) { + return function() { + return _this.bp = Breakpoints.get(); + }; + })(this)); + +}).call(this); diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js new file mode 100644 index 00000000000..fceeff36728 --- /dev/null +++ b/app/assets/javascripts/broadcast_message.js @@ -0,0 +1,34 @@ +(function() { + $(function() { + var previewPath; + $('input#broadcast_message_color').on('input', function() { + var previewColor; + previewColor = $(this).val(); + return $('div.broadcast-message-preview').css('background-color', previewColor); + }); + $('input#broadcast_message_font').on('input', function() { + var previewColor; + previewColor = $(this).val(); + return $('div.broadcast-message-preview').css('color', previewColor); + }); + previewPath = $('textarea#broadcast_message_message').data('preview-path'); + return $('textarea#broadcast_message_message').on('input', function() { + var message; + message = $(this).val(); + if (message === '') { + return $('.js-broadcast-message-preview').text("Your message here"); + } else { + return $.ajax({ + url: previewPath, + type: "POST", + data: { + broadcast_message: { + message: message + } + } + }); + } + }); + }); + +}).call(this); diff --git a/app/assets/javascripts/broadcast_message.js.coffee b/app/assets/javascripts/broadcast_message.js.coffee deleted file mode 100644 index a38a329c4c2..00000000000 --- a/app/assets/javascripts/broadcast_message.js.coffee +++ /dev/null @@ -1,22 +0,0 @@ -$ -> - $('input#broadcast_message_color').on 'input', -> - previewColor = $(@).val() - $('div.broadcast-message-preview').css('background-color', previewColor) - - $('input#broadcast_message_font').on 'input', -> - previewColor = $(@).val() - $('div.broadcast-message-preview').css('color', previewColor) - - previewPath = $('textarea#broadcast_message_message').data('preview-path') - - $('textarea#broadcast_message_message').on 'input', -> - message = $(@).val() - - if message == '' - $('.js-broadcast-message-preview').text("Your message here") - else - $.ajax( - url: previewPath - type: "POST" - data: { broadcast_message: { message: message } } - ) diff --git a/app/assets/javascripts/build.coffee b/app/assets/javascripts/build.coffee deleted file mode 100644 index cf203ea43a0..00000000000 --- a/app/assets/javascripts/build.coffee +++ /dev/null @@ -1,114 +0,0 @@ -class @Build - @interval: null - @state: null - - constructor: (@page_url, @build_url, @build_status, @state) -> - clearInterval(Build.interval) - - # Init breakpoint checker - @bp = Breakpoints.get() - @hideSidebar() - $('.js-build-sidebar').niceScroll() - $(document) - .off 'click', '.js-sidebar-build-toggle' - .on 'click', '.js-sidebar-build-toggle', @toggleSidebar - - $(window) - .off 'resize.build' - .on 'resize.build', @hideSidebar - - @updateArtifactRemoveDate() - - if $('#build-trace').length - @getInitialBuildTrace() - @initScrollButtonAffix() - - if @build_status is "running" or @build_status is "pending" - # - # Bind autoscroll button to follow build output - # - $('#autoscroll-button').on 'click', -> - state = $(this).data("state") - if "enabled" is state - $(this).data "state", "disabled" - $(this).text "enable autoscroll" - else - $(this).data "state", "enabled" - $(this).text "disable autoscroll" - - # - # Check for new build output if user still watching build page - # Only valid for runnig build when output changes during time - # - Build.interval = setInterval => - if window.location.href.split("#").first() is @page_url - @getBuildTrace() - , 4000 - - getInitialBuildTrace: -> - $.ajax - url: @build_url - dataType: 'json' - success: (build_data) -> - $('.js-build-output').html build_data.trace_html - - if build_data.status is 'success' or build_data.status is 'failed' - $('.js-build-refresh').remove() - - getBuildTrace: -> - $.ajax - url: "#{@page_url}/trace.json?state=#{encodeURIComponent(@state)}" - dataType: "json" - success: (log) => - if log.state - @state = log.state - - if log.status is "running" - if log.append - $('.js-build-output').append log.html - else - $('.js-build-output').html log.html - @checkAutoscroll() - else if log.status isnt @build_status - Turbolinks.visit @page_url - - checkAutoscroll: -> - $("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state") - - initScrollButtonAffix: -> - $buildScroll = $('#js-build-scroll') - $body = $('body') - $buildTrace = $('#build-trace') - - $buildScroll.affix( - offset: - bottom: -> - $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top) - ) - - shouldHideSidebar: -> - bootstrapBreakpoint = @bp.getBreakpointSize() - - bootstrapBreakpoint is 'xs' or bootstrapBreakpoint is 'sm' - - toggleSidebar: => - if @shouldHideSidebar() - $('.js-build-sidebar') - .toggleClass 'right-sidebar-expanded right-sidebar-collapsed' - - hideSidebar: => - if @shouldHideSidebar() - $('.js-build-sidebar') - .removeClass 'right-sidebar-expanded' - .addClass 'right-sidebar-collapsed' - else - $('.js-build-sidebar') - .removeClass 'right-sidebar-collapsed' - .addClass 'right-sidebar-expanded' - - updateArtifactRemoveDate: -> - $date = $('.js-artifacts-remove') - - if $date.length - date = $date.text() - $date.text $.timefor(new Date(date), ' ') diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js new file mode 100644 index 00000000000..e135cb92a30 --- /dev/null +++ b/app/assets/javascripts/build.js @@ -0,0 +1,139 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Build = (function() { + Build.interval = null; + + Build.state = null; + + function Build(page_url, build_url, build_status, state1) { + this.page_url = page_url; + this.build_url = build_url; + this.build_status = build_status; + this.state = state1; + this.hideSidebar = bind(this.hideSidebar, this); + this.toggleSidebar = bind(this.toggleSidebar, this); + clearInterval(Build.interval); + this.bp = Breakpoints.get(); + this.hideSidebar(); + $('.js-build-sidebar').niceScroll(); + $(document).off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar); + $(window).off('resize.build').on('resize.build', this.hideSidebar); + this.updateArtifactRemoveDate(); + if ($('#build-trace').length) { + this.getInitialBuildTrace(); + this.initScrollButtonAffix(); + } + if (this.build_status === "running" || this.build_status === "pending") { + $('#autoscroll-button').on('click', function() { + var state; + state = $(this).data("state"); + if ("enabled" === state) { + $(this).data("state", "disabled"); + return $(this).text("enable autoscroll"); + } else { + $(this).data("state", "enabled"); + return $(this).text("disable autoscroll"); + } + }); + Build.interval = setInterval((function(_this) { + return function() { + if (window.location.href.split("#").first() === _this.page_url) { + return _this.getBuildTrace(); + } + }; + })(this), 4000); + } + } + + Build.prototype.getInitialBuildTrace = function() { + return $.ajax({ + url: this.build_url, + dataType: 'json', + success: function(build_data) { + $('.js-build-output').html(build_data.trace_html); + if (build_data.status === 'success' || build_data.status === 'failed') { + return $('.js-build-refresh').remove(); + } + } + }); + }; + + Build.prototype.getBuildTrace = function() { + return $.ajax({ + url: this.page_url + "/trace.json?state=" + (encodeURIComponent(this.state)), + dataType: "json", + success: (function(_this) { + return function(log) { + if (log.state) { + _this.state = log.state; + } + if (log.status === "running") { + if (log.append) { + $('.js-build-output').append(log.html); + } else { + $('.js-build-output').html(log.html); + } + return _this.checkAutoscroll(); + } else if (log.status !== _this.build_status) { + return Turbolinks.visit(_this.page_url); + } + }; + })(this) + }); + }; + + Build.prototype.checkAutoscroll = function() { + if ("enabled" === $("#autoscroll-button").data("state")) { + return $("html,body").scrollTop($("#build-trace").height()); + } + }; + + Build.prototype.initScrollButtonAffix = function() { + var $body, $buildScroll, $buildTrace; + $buildScroll = $('#js-build-scroll'); + $body = $('body'); + $buildTrace = $('#build-trace'); + return $buildScroll.affix({ + offset: { + bottom: function() { + return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top); + } + } + }); + }; + + Build.prototype.shouldHideSidebar = function() { + var bootstrapBreakpoint; + bootstrapBreakpoint = this.bp.getBreakpointSize(); + return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; + }; + + Build.prototype.toggleSidebar = function() { + if (this.shouldHideSidebar()) { + return $('.js-build-sidebar').toggleClass('right-sidebar-expanded right-sidebar-collapsed'); + } + }; + + Build.prototype.hideSidebar = function() { + if (this.shouldHideSidebar()) { + return $('.js-build-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); + } else { + return $('.js-build-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); + } + }; + + Build.prototype.updateArtifactRemoveDate = function() { + var $date, date; + $date = $('.js-artifacts-remove'); + if ($date.length) { + date = $date.text(); + return $date.text($.timefor(new Date(date), ' ')); + } + }; + + return Build; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js new file mode 100644 index 00000000000..f345ba0abe6 --- /dev/null +++ b/app/assets/javascripts/build_artifacts.js @@ -0,0 +1,27 @@ +(function() { + this.BuildArtifacts = (function() { + function BuildArtifacts() { + this.disablePropagation(); + this.setupEntryClick(); + } + + BuildArtifacts.prototype.disablePropagation = function() { + $('.top-block').on('click', '.download', function(e) { + return e.stopPropagation(); + }); + return $('.tree-holder').on('click', 'tr[data-link] a', function(e) { + return e.stopImmediatePropagation(); + }); + }; + + BuildArtifacts.prototype.setupEntryClick = function() { + return $('.tree-holder').on('click', 'tr[data-link]', function(e) { + return window.location = this.dataset.link; + }); + }; + + return BuildArtifacts; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/build_artifacts.js.coffee b/app/assets/javascripts/build_artifacts.js.coffee deleted file mode 100644 index 5ae6cba56c8..00000000000 --- a/app/assets/javascripts/build_artifacts.js.coffee +++ /dev/null @@ -1,14 +0,0 @@ -class @BuildArtifacts - constructor: () -> - @disablePropagation() - @setupEntryClick() - - disablePropagation: -> - $('.top-block').on 'click', '.download', (e) -> - e.stopPropagation() - $('.tree-holder').on 'click', 'tr[data-link] a', (e) -> - e.stopImmediatePropagation() - - setupEntryClick: -> - $('.tree-holder').on 'click', 'tr[data-link]', (e) -> - window.location = @dataset.link diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js new file mode 100644 index 00000000000..23cf5b519f4 --- /dev/null +++ b/app/assets/javascripts/commit.js @@ -0,0 +1,13 @@ +(function() { + this.Commit = (function() { + function Commit() { + $('.files .diff-file').each(function() { + return new CommitFile(this); + }); + } + + return Commit; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/commit.js.coffee b/app/assets/javascripts/commit.js.coffee deleted file mode 100644 index 0566e239191..00000000000 --- a/app/assets/javascripts/commit.js.coffee +++ /dev/null @@ -1,4 +0,0 @@ -class @Commit - constructor: -> - $('.files .diff-file').each -> - new CommitFile(this) diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js new file mode 100644 index 00000000000..be24ee56aad --- /dev/null +++ b/app/assets/javascripts/commit/file.js @@ -0,0 +1,13 @@ +(function() { + this.CommitFile = (function() { + function CommitFile(file) { + if ($('.image', file).length) { + new ImageFile(file); + } + } + + return CommitFile; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/commit/file.js.coffee b/app/assets/javascripts/commit/file.js.coffee deleted file mode 100644 index 83e793863b6..00000000000 --- a/app/assets/javascripts/commit/file.js.coffee +++ /dev/null @@ -1,5 +0,0 @@ -class @CommitFile - - constructor: (file) -> - if $('.image', file).length - new ImageFile(file) diff --git a/app/assets/javascripts/commit/image-file.js b/app/assets/javascripts/commit/image-file.js new file mode 100644 index 00000000000..c0d0b2d049f --- /dev/null +++ b/app/assets/javascripts/commit/image-file.js @@ -0,0 +1,175 @@ +(function() { + this.ImageFile = (function() { + var prepareFrames; + + ImageFile.availWidth = 900; + + ImageFile.viewModes = ['two-up', 'swipe']; + + function ImageFile(file) { + this.file = file; + this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), (function(_this) { + return function(deletedWidth, deletedHeight) { + return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(width, height) { + if (width === deletedWidth && height === deletedHeight) { + return _this.initViewModes(); + } else { + return _this.initView('two-up'); + } + }); + }; + })(this)); + } + + ImageFile.prototype.initViewModes = function() { + var viewMode; + viewMode = ImageFile.viewModes[0]; + $('.view-modes', this.file).removeClass('hide'); + $('.view-modes-menu', this.file).on('click', 'li', (function(_this) { + return function(event) { + if (!$(event.currentTarget).hasClass('active')) { + return _this.activateViewMode(event.currentTarget.className); + } + }; + })(this)); + return this.activateViewMode(viewMode); + }; + + ImageFile.prototype.activateViewMode = function(viewMode) { + $('.view-modes-menu li', this.file).removeClass('active').filter("." + viewMode).addClass('active'); + return $(".view:visible:not(." + viewMode + ")", this.file).fadeOut(200, (function(_this) { + return function() { + $(".view." + viewMode, _this.file).fadeIn(200); + return _this.initView(viewMode); + }; + })(this)); + }; + + ImageFile.prototype.initView = function(viewMode) { + return this.views[viewMode].call(this); + }; + + prepareFrames = function(view) { + var maxHeight, maxWidth; + maxWidth = 0; + maxHeight = 0; + $('.frame', view).each((function(_this) { + return function(index, frame) { + var height, width; + width = $(frame).width(); + height = $(frame).height(); + maxWidth = width > maxWidth ? width : maxWidth; + return maxHeight = height > maxHeight ? height : maxHeight; + }; + })(this)).css({ + width: maxWidth, + height: maxHeight + }); + return [maxWidth, maxHeight]; + }; + + ImageFile.prototype.views = { + 'two-up': function() { + return $('.two-up.view .wrap', this.file).each((function(_this) { + return function(index, wrap) { + $('img', wrap).each(function() { + var currentWidth; + currentWidth = $(this).width(); + if (currentWidth > ImageFile.availWidth / 2) { + return $(this).width(ImageFile.availWidth / 2); + } + }); + return _this.requestImageInfo($('img', wrap), function(width, height) { + $('.image-info .meta-width', wrap).text(width + "px"); + $('.image-info .meta-height', wrap).text(height + "px"); + return $('.image-info', wrap).removeClass('hide'); + }); + }; + })(this)); + }, + 'swipe': function() { + var maxHeight, maxWidth; + maxWidth = 0; + maxHeight = 0; + return $('.swipe.view', this.file).each((function(_this) { + return function(index, view) { + var ref; + ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; + $('.swipe-frame', view).css({ + width: maxWidth + 16, + height: maxHeight + 28 + }); + $('.swipe-wrap', view).css({ + width: maxWidth + 1, + height: maxHeight + 2 + }); + return $('.swipe-bar', view).css({ + left: 0 + }).draggable({ + axis: 'x', + containment: 'parent', + drag: function(event) { + return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left); + }, + stop: function(event) { + return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left); + } + }); + }; + })(this)); + }, + 'onion-skin': function() { + var dragTrackWidth, maxHeight, maxWidth; + maxWidth = 0; + maxHeight = 0; + dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width(); + return $('.onion-skin.view', this.file).each((function(_this) { + return function(index, view) { + var ref; + ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; + $('.onion-skin-frame', view).css({ + width: maxWidth + 16, + height: maxHeight + 28 + }); + $('.swipe-wrap', view).css({ + width: maxWidth + 1, + height: maxHeight + 2 + }); + return $('.dragger', view).css({ + left: dragTrackWidth + }).draggable({ + axis: 'x', + containment: 'parent', + drag: function(event) { + return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth); + }, + stop: function(event) { + return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth); + } + }); + }; + })(this)); + } + }; + + ImageFile.prototype.requestImageInfo = function(img, callback) { + var domImg; + domImg = img.get(0); + if (domImg) { + if (domImg.complete) { + return callback.call(this, domImg.naturalWidth, domImg.naturalHeight); + } else { + return img.on('load', (function(_this) { + return function() { + return callback.call(_this, domImg.naturalWidth, domImg.naturalHeight); + }; + })(this)); + } + } + }; + + return ImageFile; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/commit/image-file.js.coffee b/app/assets/javascripts/commit/image-file.js.coffee deleted file mode 100644 index 9c723f51e54..00000000000 --- a/app/assets/javascripts/commit/image-file.js.coffee +++ /dev/null @@ -1,127 +0,0 @@ -class @ImageFile - - # Width where images must fits in, for 2-up this gets divided by 2 - @availWidth = 900 - @viewModes = ['two-up', 'swipe'] - - constructor: (@file) -> - # Determine if old and new file has same dimensions, if not show 'two-up' view - this.requestImageInfo $('.two-up.view .frame.deleted img', @file), (deletedWidth, deletedHeight) => - this.requestImageInfo $('.two-up.view .frame.added img', @file), (width, height) => - if width == deletedWidth && height == deletedHeight - this.initViewModes() - else - this.initView('two-up') - - initViewModes: -> - viewMode = ImageFile.viewModes[0] - - $('.view-modes', @file).removeClass 'hide' - $('.view-modes-menu', @file).on 'click', 'li', (event) => - unless $(event.currentTarget).hasClass('active') - this.activateViewMode(event.currentTarget.className) - - this.activateViewMode(viewMode) - - activateViewMode: (viewMode) -> - $('.view-modes-menu li', @file) - .removeClass('active') - .filter(".#{viewMode}").addClass 'active' - $(".view:visible:not(.#{viewMode})", @file).fadeOut 200, => - $(".view.#{viewMode}", @file).fadeIn(200) - this.initView viewMode - - initView: (viewMode) -> - this.views[viewMode].call(this) - - prepareFrames = (view) -> - maxWidth = 0 - maxHeight = 0 - $('.frame', view).each (index, frame) => - width = $(frame).width() - height = $(frame).height() - maxWidth = if width > maxWidth then width else maxWidth - maxHeight = if height > maxHeight then height else maxHeight - .css - width: maxWidth - height: maxHeight - - [maxWidth, maxHeight] - - views: - 'two-up': -> - $('.two-up.view .wrap', @file).each (index, wrap) => - $('img', wrap).each -> - currentWidth = $(this).width() - if currentWidth > ImageFile.availWidth / 2 - $(this).width ImageFile.availWidth / 2 - - this.requestImageInfo $('img', wrap), (width, height) -> - $('.image-info .meta-width', wrap).text "#{width}px" - $('.image-info .meta-height', wrap).text "#{height}px" - $('.image-info', wrap).removeClass('hide') - - 'swipe': -> - maxWidth = 0 - maxHeight = 0 - - $('.swipe.view', @file).each (index, view) => - - [maxWidth, maxHeight] = prepareFrames(view) - - $('.swipe-frame', view).css - width: maxWidth + 16 - height: maxHeight + 28 - - $('.swipe-wrap', view).css - width: maxWidth + 1 - height: maxHeight + 2 - - $('.swipe-bar', view).css - left: 0 - .draggable - axis: 'x' - containment: 'parent' - drag: (event) -> - $('.swipe-wrap', view).width (maxWidth + 1) - $(this).position().left - stop: (event) -> - $('.swipe-wrap', view).width (maxWidth + 1) - $(this).position().left - - 'onion-skin': -> - maxWidth = 0 - maxHeight = 0 - - dragTrackWidth = $('.drag-track', @file).width() - $('.dragger', @file).width() - - $('.onion-skin.view', @file).each (index, view) => - - [maxWidth, maxHeight] = prepareFrames(view) - - $('.onion-skin-frame', view).css - width: maxWidth + 16 - height: maxHeight + 28 - - $('.swipe-wrap', view).css - width: maxWidth + 1 - height: maxHeight + 2 - - $('.dragger', view).css - left: dragTrackWidth - .draggable - axis: 'x' - containment: 'parent' - drag: (event) -> - $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth) - stop: (event) -> - $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth) - - - - requestImageInfo: (img, callback) -> - domImg = img.get(0) - if domImg - if domImg.complete - callback.call(this, domImg.naturalWidth, domImg.naturalHeight) - else - img.on 'load', => - callback.call(this, domImg.naturalWidth, domImg.naturalHeight) diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js new file mode 100644 index 00000000000..37f168c5190 --- /dev/null +++ b/app/assets/javascripts/commits.js @@ -0,0 +1,58 @@ +(function() { + this.CommitsList = (function() { + function CommitsList() {} + + CommitsList.timer = null; + + CommitsList.init = function(limit) { + $("body").on("click", ".day-commits-table li.commit", function(event) { + if (event.target.nodeName !== "A") { + location.href = $(this).attr("url"); + e.stopPropagation(); + return false; + } + }); + Pager.init(limit, false); + this.content = $("#commits-list"); + this.searchField = $("#commits-search"); + return this.initSearch(); + }; + + CommitsList.initSearch = function() { + this.timer = null; + return this.searchField.keyup((function(_this) { + return function() { + clearTimeout(_this.timer); + return _this.timer = setTimeout(_this.filterResults, 500); + }; + })(this)); + }; + + CommitsList.filterResults = function() { + var commitsUrl, form, search; + form = $(".commits-search-form"); + search = CommitsList.searchField.val(); + commitsUrl = form.attr("action") + '?' + form.serialize(); + CommitsList.content.fadeTo('fast', 0.5); + return $.ajax({ + type: "GET", + url: form.attr("action"), + data: form.serialize(), + complete: function() { + return CommitsList.content.fadeTo('fast', 1.0); + }, + success: function(data) { + CommitsList.content.html(data.html); + return history.replaceState({ + page: commitsUrl + }, document.title, commitsUrl); + }, + dataType: "json" + }); + }; + + return CommitsList; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/commits.js.coffee b/app/assets/javascripts/commits.js.coffee deleted file mode 100644 index 0acb4c1955e..00000000000 --- a/app/assets/javascripts/commits.js.coffee +++ /dev/null @@ -1,39 +0,0 @@ -class @CommitsList - @timer = null - - @init: (limit) -> - $("body").on "click", ".day-commits-table li.commit", (event) -> - if event.target.nodeName != "A" - location.href = $(this).attr("url") - e.stopPropagation() - return false - - Pager.init limit, false - - @content = $("#commits-list") - @searchField = $("#commits-search") - @initSearch() - - @initSearch: -> - @timer = null - @searchField.keyup => - clearTimeout(@timer) - @timer = setTimeout(@filterResults, 500) - - @filterResults: => - form = $(".commits-search-form") - search = @searchField.val() - commitsUrl = form.attr("action") + '?' + form.serialize() - @content.fadeTo('fast', 0.5) - - $.ajax - type: "GET" - url: form.attr("action") - data: form.serialize() - complete: => - @content.fadeTo('fast', 1.0) - success: (data) => - @content.html(data.html) - # Change url so if user reload a page - search results are saved - history.replaceState {page: commitsUrl}, document.title, commitsUrl - dataType: "json" diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js new file mode 100644 index 00000000000..342ac0e8e69 --- /dev/null +++ b/app/assets/javascripts/compare.js @@ -0,0 +1,91 @@ +(function() { + this.Compare = (function() { + function Compare(opts) { + this.opts = opts; + this.source_loading = $(".js-source-loading"); + this.target_loading = $(".js-target-loading"); + $('.js-compare-dropdown').each((function(_this) { + return function(i, dropdown) { + var $dropdown; + $dropdown = $(dropdown); + return $dropdown.glDropdown({ + selectable: true, + fieldName: $dropdown.data('field-name'), + filterable: true, + id: function(obj, $el) { + return $el.data('id'); + }, + toggleLabel: function(obj, $el) { + return $el.text().trim(); + }, + clicked: function(e, el) { + if ($dropdown.is('.js-target-branch')) { + return _this.getTargetHtml(); + } else if ($dropdown.is('.js-source-branch')) { + return _this.getSourceHtml(); + } else if ($dropdown.is('.js-target-project')) { + return _this.getTargetProject(); + } + } + }); + }; + })(this)); + this.initialState(); + } + + Compare.prototype.initialState = function() { + this.getSourceHtml(); + return this.getTargetHtml(); + }; + + Compare.prototype.getTargetProject = function() { + return $.ajax({ + url: this.opts.targetProjectUrl, + data: { + target_project_id: $("input[name='merge_request[target_project_id]']").val() + }, + beforeSend: function() { + return $('.mr_target_commit').empty(); + }, + success: function(html) { + return $('.js-target-branch-dropdown .dropdown-content').html(html); + } + }); + }; + + Compare.prototype.getSourceHtml = function() { + return this.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', { + ref: $("input[name='merge_request[source_branch]']").val() + }); + }; + + Compare.prototype.getTargetHtml = function() { + return this.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', { + target_project_id: $("input[name='merge_request[target_project_id]']").val(), + ref: $("input[name='merge_request[target_branch]']").val() + }); + }; + + Compare.prototype.sendAjax = function(url, loading, target, data) { + var $target; + $target = $(target); + return $.ajax({ + url: url, + data: data, + beforeSend: function() { + loading.show(); + return $target.empty(); + }, + success: function(html) { + loading.hide(); + $target.html(html); + return $('.js-timeago', $target).timeago(); + } + }); + }; + + return Compare; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/compare.js.coffee b/app/assets/javascripts/compare.js.coffee deleted file mode 100644 index f20992ead3e..00000000000 --- a/app/assets/javascripts/compare.js.coffee +++ /dev/null @@ -1,67 +0,0 @@ -class @Compare - constructor: (@opts) -> - @source_loading = $ ".js-source-loading" - @target_loading = $ ".js-target-loading" - - $('.js-compare-dropdown').each (i, dropdown) => - $dropdown = $(dropdown) - - $dropdown.glDropdown( - selectable: true - fieldName: $dropdown.data 'field-name' - filterable: true - id: (obj, $el) -> - $el.data 'id' - toggleLabel: (obj, $el) -> - $el.text().trim() - clicked: (e, el) => - if $dropdown.is '.js-target-branch' - @getTargetHtml() - else if $dropdown.is '.js-source-branch' - @getSourceHtml() - else if $dropdown.is '.js-target-project' - @getTargetProject() - ) - - @initialState() - - initialState: -> - @getSourceHtml() - @getTargetHtml() - - getTargetProject: -> - $.ajax( - url: @opts.targetProjectUrl - data: - target_project_id: $("input[name='merge_request[target_project_id]']").val() - beforeSend: -> - $('.mr_target_commit').empty() - success: (html) -> - $('.js-target-branch-dropdown .dropdown-content').html html - ) - - getSourceHtml: -> - @sendAjax(@opts.sourceBranchUrl, @source_loading, '.mr_source_commit', - ref: $("input[name='merge_request[source_branch]']").val() - ) - - getTargetHtml: -> - @sendAjax(@opts.targetBranchUrl, @target_loading, '.mr_target_commit', - target_project_id: $("input[name='merge_request[target_project_id]']").val() - ref: $("input[name='merge_request[target_branch]']").val() - ) - - sendAjax: (url, loading, target, data) -> - $target = $(target) - - $.ajax( - url: url - data: data - beforeSend: -> - loading.show() - $target.empty() - success: (html) -> - loading.hide() - $target.html html - $('.js-timeago', $target).timeago() - ) diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js new file mode 100644 index 00000000000..4e3a28cd163 --- /dev/null +++ b/app/assets/javascripts/compare_autocomplete.js @@ -0,0 +1,51 @@ +(function() { + this.CompareAutocomplete = (function() { + function CompareAutocomplete() { + this.initDropdown(); + } + + CompareAutocomplete.prototype.initDropdown = function() { + return $('.js-compare-dropdown').each(function() { + var $dropdown, selected; + $dropdown = $(this); + selected = $dropdown.data('selected'); + return $dropdown.glDropdown({ + data: function(term, callback) { + return $.ajax({ + url: $dropdown.data('refs-url'), + data: { + ref: $dropdown.data('ref') + } + }).done(function(refs) { + return callback(refs); + }); + }, + selectable: true, + filterable: true, + filterByText: true, + fieldName: $dropdown.attr('name'), + filterInput: 'input[type="text"]', + renderRow: function(ref) { + var link; + if (ref.header != null) { + return $('<li />').addClass('dropdown-header').text(ref.header); + } else { + link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref)); + return $('<li />').append(link); + } + }, + id: function(obj, $el) { + return $el.attr('data-ref'); + }, + toggleLabel: function(obj, $el) { + return $el.text().trim(); + } + }); + }); + }; + + return CompareAutocomplete; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/compare_autocomplete.js.coffee b/app/assets/javascripts/compare_autocomplete.js.coffee deleted file mode 100644 index 7ad9fd97637..00000000000 --- a/app/assets/javascripts/compare_autocomplete.js.coffee +++ /dev/null @@ -1,41 +0,0 @@ -class @CompareAutocomplete - constructor: -> - @initDropdown() - - initDropdown: -> - $('.js-compare-dropdown').each -> - $dropdown = $(@) - selected = $dropdown.data('selected') - - $dropdown.glDropdown( - data: (term, callback) -> - $.ajax( - url: $dropdown.data('refs-url') - data: - ref: $dropdown.data('ref') - ).done (refs) -> - callback(refs) - selectable: true - filterable: true - filterByText: true - fieldName: $dropdown.attr('name') - filterInput: 'input[type="text"]' - renderRow: (ref) -> - if ref.header? - $('<li />') - .addClass('dropdown-header') - .text(ref.header) - else - link = $('<a />') - .attr('href', '#') - .addClass(if ref is selected then 'is-active' else '') - .text(ref) - .attr('data-ref', escape(ref)) - - $('<li />') - .append(link) - id: (obj, $el) -> - $el.attr('data-ref') - toggleLabel: (obj, $el) -> - $el.text().trim() - ) diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js new file mode 100644 index 00000000000..708ab08ffac --- /dev/null +++ b/app/assets/javascripts/confirm_danger_modal.js @@ -0,0 +1,32 @@ +(function() { + this.ConfirmDangerModal = (function() { + function ConfirmDangerModal(form, text) { + var project_path, submit; + this.form = form; + $('.js-confirm-text').text(text || ''); + $('.js-confirm-danger-input').val(''); + $('#modal-confirm-danger').modal('show'); + project_path = $('.js-confirm-danger-match').text(); + submit = $('.js-confirm-danger-submit'); + submit.disable(); + $('.js-confirm-danger-input').off('input'); + $('.js-confirm-danger-input').on('input', function() { + if (rstrip($(this).val()) === project_path) { + return submit.enable(); + } else { + return submit.disable(); + } + }); + $('.js-confirm-danger-submit').off('click'); + $('.js-confirm-danger-submit').on('click', (function(_this) { + return function() { + return _this.form.submit(); + }; + })(this)); + } + + return ConfirmDangerModal; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/confirm_danger_modal.js.coffee b/app/assets/javascripts/confirm_danger_modal.js.coffee deleted file mode 100644 index 66e34dd4a08..00000000000 --- a/app/assets/javascripts/confirm_danger_modal.js.coffee +++ /dev/null @@ -1,20 +0,0 @@ -class @ConfirmDangerModal - constructor: (form, text) -> - @form = form - $('.js-confirm-text').text(text || '') - $('.js-confirm-danger-input').val('') - $('#modal-confirm-danger').modal('show') - project_path = $('.js-confirm-danger-match').text() - submit = $('.js-confirm-danger-submit') - submit.disable() - - $('.js-confirm-danger-input').off 'input' - $('.js-confirm-danger-input').on 'input', -> - if rstrip($(@).val()) is project_path - submit.enable() - else - submit.disable() - - $('.js-confirm-danger-submit').off 'click' - $('.js-confirm-danger-submit').on 'click', => - @form.submit() diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js new file mode 100644 index 00000000000..c82798cc6a5 --- /dev/null +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -0,0 +1,42 @@ + +/*= require clipboard */ + +(function() { + var genericError, genericSuccess, showTooltip; + + genericSuccess = function(e) { + showTooltip(e.trigger, 'Copied!'); + e.clearSelection(); + return $(e.trigger).blur(); + }; + + genericError = function(e) { + var key; + if (/Mac/i.test(navigator.userAgent)) { + key = '⌘'; + } else { + key = 'Ctrl'; + } + return showTooltip(e.trigger, "Press " + key + "-C to copy"); + }; + + showTooltip = function(target, title) { + return $(target).tooltip({ + container: 'body', + html: 'true', + placement: 'auto bottom', + title: title, + trigger: 'manual' + }).tooltip('show').one('mouseleave', function() { + return $(this).tooltip('hide'); + }); + }; + + $(function() { + var clipboard; + clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]'); + clipboard.on('success', genericSuccess); + return clipboard.on('error', genericError); + }); + +}).call(this); diff --git a/app/assets/javascripts/copy_to_clipboard.js.coffee b/app/assets/javascripts/copy_to_clipboard.js.coffee deleted file mode 100644 index 24301e01b10..00000000000 --- a/app/assets/javascripts/copy_to_clipboard.js.coffee +++ /dev/null @@ -1,37 +0,0 @@ -#= require clipboard - -genericSuccess = (e) -> - showTooltip(e.trigger, 'Copied!') - - # Clear the selection and blur the trigger so it loses its border - e.clearSelection() - $(e.trigger).blur() - -# Safari doesn't support `execCommand`, so instead we inform the user to -# copy manually. -# -# See http://clipboardjs.com/#browser-support -genericError = (e) -> - if /Mac/i.test(navigator.userAgent) - key = '⌘' # Command - else - key = 'Ctrl' - - showTooltip(e.trigger, "Press #{key}-C to copy") - -showTooltip = (target, title) -> - $(target). - tooltip( - container: 'body' - html: 'true' - placement: 'auto bottom' - title: title - trigger: 'manual' - ). - tooltip('show'). - one('mouseleave', -> $(this).tooltip('hide')) - -$ -> - clipboard = new Clipboard '[data-clipboard-target], [data-clipboard-text]' - clipboard.on 'success', genericSuccess - clipboard.on 'error', genericError diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js new file mode 100644 index 00000000000..298f3852085 --- /dev/null +++ b/app/assets/javascripts/diff.js @@ -0,0 +1,77 @@ +(function() { + this.Diff = (function() { + var UNFOLD_COUNT; + + UNFOLD_COUNT = 20; + + function Diff() { + $('.files .diff-file').singleFileDiff(); + this.filesCommentButton = $('.files .diff-file').filesCommentButton(); + $(document).off('click', '.js-unfold'); + $(document).on('click', '.js-unfold', (function(_this) { + return function(event) { + var line_number, link, offset, old_line, params, prev_new_line, prev_old_line, ref, ref1, since, target, to, unfold, unfoldBottom; + target = $(event.target); + unfoldBottom = target.hasClass('js-unfold-bottom'); + unfold = true; + ref = _this.lineNumbers(target.parent()), old_line = ref[0], line_number = ref[1]; + offset = line_number - old_line; + if (unfoldBottom) { + line_number += 1; + since = line_number; + to = line_number + UNFOLD_COUNT; + } else { + ref1 = _this.lineNumbers(target.parent().prev()), prev_old_line = ref1[0], prev_new_line = ref1[1]; + line_number -= 1; + to = line_number; + if (line_number - UNFOLD_COUNT > prev_new_line + 1) { + since = line_number - UNFOLD_COUNT; + } else { + since = prev_new_line + 1; + unfold = false; + } + } + link = target.parents('.diff-file').attr('data-blob-diff-path'); + params = { + since: since, + to: to, + bottom: unfoldBottom, + offset: offset, + unfold: unfold, + indent: 1 + }; + return $.get(link, params, function(response) { + return target.parent().replaceWith(response); + }); + }; + })(this)); + } + + Diff.prototype.lineNumbers = function(line) { + var i, l, len, line_number, line_numbers, lines, results; + if (!line.children().length) { + return [0, 0]; + } + lines = line.children().slice(0, 2); + line_numbers = (function() { + var i, len, results; + results = []; + for (i = 0, len = lines.length; i < len; i++) { + l = lines[i]; + results.push($(l).attr('data-linenumber')); + } + return results; + })(); + results = []; + for (i = 0, len = line_numbers.length; i < len; i++) { + line_number = line_numbers[i]; + results.push(parseInt(line_number)); + } + return results; + }; + + return Diff; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/diff.js.coffee b/app/assets/javascripts/diff.js.coffee deleted file mode 100644 index c132cc8c542..00000000000 --- a/app/assets/javascripts/diff.js.coffee +++ /dev/null @@ -1,51 +0,0 @@ -class @Diff - UNFOLD_COUNT = 20 - constructor: -> - $('.files .diff-file').singleFileDiff() - @filesCommentButton = $('.files .diff-file').filesCommentButton() - - $(document).off('click', '.js-unfold') - $(document).on('click', '.js-unfold', (event) => - target = $(event.target) - unfoldBottom = target.hasClass('js-unfold-bottom') - unfold = true - - [old_line, line_number] = @lineNumbers(target.parent()) - offset = line_number - old_line - - if unfoldBottom - line_number += 1 - since = line_number - to = line_number + UNFOLD_COUNT - else - [prev_old_line, prev_new_line] = @lineNumbers(target.parent().prev()) - line_number -= 1 - to = line_number - if line_number - UNFOLD_COUNT > prev_new_line + 1 - since = line_number - UNFOLD_COUNT - else - since = prev_new_line + 1 - unfold = false - - link = target.parents('.diff-file').attr('data-blob-diff-path') - params = - since: since - to: to - bottom: unfoldBottom - offset: offset - unfold: unfold - # indent is used to compensate for single space indent to fit - # '+' and '-' prepended to diff lines, - # see https://gitlab.com/gitlab-org/gitlab-ce/issues/707 - indent: 1 - - $.get(link, params, (response) -> - target.parent().replaceWith(response) - ) - ) - - lineNumbers: (line) -> - return ([0, 0]) unless line.children().length - lines = line.children().slice(0, 2) - line_numbers = ($(l).attr('data-linenumber') for l in lines) - (parseInt(line_number) for line_number in line_numbers) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js new file mode 100644 index 00000000000..d212d66da1b --- /dev/null +++ b/app/assets/javascripts/dispatcher.js @@ -0,0 +1,255 @@ +(function() { + var Dispatcher; + + $(function() { + return new Dispatcher(); + }); + + Dispatcher = (function() { + function Dispatcher() { + this.initSearch(); + this.initPageScripts(); + } + + Dispatcher.prototype.initPageScripts = function() { + var page, path, shortcut_handler; + page = $('body').attr('data-page'); + if (!page) { + return false; + } + path = page.split(':'); + shortcut_handler = null; + switch (page) { + case 'projects:issues:index': + Issuable.init(); + new IssuableBulkActions(); + shortcut_handler = new ShortcutsNavigation(); + break; + case 'projects:issues:show': + new Issue(); + shortcut_handler = new ShortcutsIssuable(); + new ZenMode(); + break; + case 'projects:milestones:show': + case 'groups:milestones:show': + case 'dashboard:milestones:show': + new Milestone(); + break; + case 'dashboard:todos:index': + new Todos(); + break; + case 'projects:milestones:new': + case 'projects:milestones:edit': + new ZenMode(); + new DueDateSelect(); + new GLForm($('.milestone-form')); + break; + case 'groups:milestones:new': + new ZenMode(); + break; + case 'projects:compare:show': + new Diff(); + break; + case 'projects:issues:new': + case 'projects:issues:edit': + shortcut_handler = new ShortcutsNavigation(); + new GLForm($('.issue-form')); + new IssuableForm($('.issue-form')); + break; + case 'projects:merge_requests:new': + case 'projects:merge_requests:edit': + new Diff(); + shortcut_handler = new ShortcutsNavigation(); + new GLForm($('.merge-request-form')); + new IssuableForm($('.merge-request-form')); + break; + case 'projects:tags:new': + new ZenMode(); + new GLForm($('.tag-form')); + break; + case 'projects:releases:edit': + new ZenMode(); + new GLForm($('.release-form')); + break; + case 'projects:merge_requests:show': + new Diff(); + shortcut_handler = new ShortcutsIssuable(true); + new ZenMode(); + new MergedButtons(); + break; + case 'projects:merge_requests:commits': + case 'projects:merge_requests:builds': + new MergedButtons(); + break; + case "projects:merge_requests:diffs": + new Diff(); + new ZenMode(); + new MergedButtons(); + break; + case 'projects:merge_requests:index': + shortcut_handler = new ShortcutsNavigation(); + Issuable.init(); + break; + case 'dashboard:activity': + new Activities(); + break; + case 'dashboard:projects:starred': + new Activities(); + break; + case 'projects:commit:show': + new Commit(); + new Diff(); + new ZenMode(); + shortcut_handler = new ShortcutsNavigation(); + break; + case 'projects:commits:show': + case 'projects:activity': + shortcut_handler = new ShortcutsNavigation(); + break; + case 'projects:show': + shortcut_handler = new ShortcutsNavigation(); + new NotificationsForm(); + if ($('#tree-slider').length) { + new TreeView(); + } + break; + case 'groups:activity': + new Activities(); + break; + case 'groups:show': + shortcut_handler = new ShortcutsNavigation(); + new NotificationsForm(); + new NotificationsDropdown(); + break; + case 'groups:group_members:index': + new GroupMembers(); + new UsersSelect(); + break; + case 'projects:project_members:index': + new ProjectMembers(); + new UsersSelect(); + break; + case 'groups:new': + case 'groups:edit': + case 'admin:groups:edit': + case 'admin:groups:new': + new GroupAvatar(); + break; + case 'projects:tree:show': + shortcut_handler = new ShortcutsNavigation(); + new TreeView(); + break; + case 'projects:find_file:show': + shortcut_handler = true; + break; + case 'projects:blob:show': + case 'projects:blame:show': + new LineHighlighter(); + shortcut_handler = new ShortcutsNavigation(); + new ShortcutsBlob(true); + break; + case 'projects:labels:new': + case 'projects:labels:edit': + new Labels(); + break; + case 'projects:labels:index': + if ($('.prioritized-labels').length) { + new LabelManager(); + } + break; + case 'projects:network:show': + shortcut_handler = true; + break; + case 'projects:forks:new': + new ProjectFork(); + break; + case 'projects:artifacts:browse': + new BuildArtifacts(); + break; + case 'projects:group_links:index': + new GroupsSelect(); + break; + case 'search:show': + new Search(); + } + switch (path.first()) { + case 'admin': + new Admin(); + switch (path[1]) { + case 'groups': + new UsersSelect(); + break; + case 'projects': + new NamespaceSelects(); + } + break; + case 'dashboard': + case 'root': + shortcut_handler = new ShortcutsDashboardNavigation(); + break; + case 'profiles': + new NotificationsForm(); + new NotificationsDropdown(); + break; + case 'projects': + new Project(); + new ProjectAvatar(); + switch (path[1]) { + case 'compare': + new CompareAutocomplete(); + break; + case 'edit': + shortcut_handler = new ShortcutsNavigation(); + new ProjectNew(); + break; + case 'new': + new ProjectNew(); + break; + case 'show': + new ProjectNew(); + new ProjectShow(); + new NotificationsDropdown(); + break; + case 'wikis': + new Wikis(); + shortcut_handler = new ShortcutsNavigation(); + new ZenMode(); + new GLForm($('.wiki-form')); + break; + case 'snippets': + shortcut_handler = new ShortcutsNavigation(); + if (path[2] === 'show') { + new ZenMode(); + } + break; + case 'labels': + case 'graphs': + case 'compare': + case 'pipelines': + case 'forks': + case 'milestones': + case 'project_members': + case 'deploy_keys': + case 'builds': + case 'hooks': + case 'services': + case 'protected_branches': + shortcut_handler = new ShortcutsNavigation(); + } + } + if (!shortcut_handler) { + return new Shortcuts(); + } + }; + + Dispatcher.prototype.initSearch = function() { + if ($('.search').length) { + return new SearchAutocomplete(); + } + }; + + return Dispatcher; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee deleted file mode 100644 index afaa6407b05..00000000000 --- a/app/assets/javascripts/dispatcher.js.coffee +++ /dev/null @@ -1,171 +0,0 @@ -$ -> - new Dispatcher() - -class Dispatcher - constructor: () -> - @initSearch() - @initPageScripts() - - initPageScripts: -> - page = $('body').attr('data-page') - - unless page - return false - - path = page.split(':') - shortcut_handler = null - switch page - when 'projects:issues:index' - Issuable.init() - new IssuableBulkActions() - shortcut_handler = new ShortcutsNavigation() - when 'projects:issues:show' - new Issue() - shortcut_handler = new ShortcutsIssuable() - new ZenMode() - when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show' - new Milestone() - when 'dashboard:todos:index' - new Todos() - when 'projects:milestones:new', 'projects:milestones:edit' - new ZenMode() - new DueDateSelect() - new GLForm($('.milestone-form')) - when 'groups:milestones:new' - new ZenMode() - when 'projects:compare:show' - new Diff() - when 'projects:issues:new','projects:issues:edit' - shortcut_handler = new ShortcutsNavigation() - new GLForm($('.issue-form')) - new IssuableForm($('.issue-form')) - when 'projects:merge_requests:new', 'projects:merge_requests:edit' - new Diff() - shortcut_handler = new ShortcutsNavigation() - new GLForm($('.merge-request-form')) - new IssuableForm($('.merge-request-form')) - when 'projects:tags:new' - new ZenMode() - new GLForm($('.tag-form')) - when 'projects:releases:edit' - new ZenMode() - new GLForm($('.release-form')) - when 'projects:merge_requests:show' - new Diff() - shortcut_handler = new ShortcutsIssuable(true) - new ZenMode() - new MergedButtons() - when 'projects:merge_requests:commits', 'projects:merge_requests:builds' - new MergedButtons() - when "projects:merge_requests:diffs" - new Diff() - new ZenMode() - new MergedButtons() - when 'projects:merge_requests:index' - shortcut_handler = new ShortcutsNavigation() - Issuable.init() - when 'dashboard:activity' - new Activities() - when 'dashboard:projects:starred' - new Activities() - when 'projects:commit:show' - new Commit() - new Diff() - new ZenMode() - shortcut_handler = new ShortcutsNavigation() - when 'projects:commits:show', 'projects:activity' - shortcut_handler = new ShortcutsNavigation() - when 'projects:show' - shortcut_handler = new ShortcutsNavigation() - - new NotificationsForm() - new TreeView() if $('#tree-slider').length - when 'groups:activity' - new Activities() - when 'groups:show' - shortcut_handler = new ShortcutsNavigation() - new NotificationsForm() - new NotificationsDropdown() - when 'groups:group_members:index' - new GroupMembers() - new UsersSelect() - when 'projects:project_members:index' - new ProjectMembers() - new UsersSelect() - when 'groups:new', 'groups:edit', 'admin:groups:edit', 'admin:groups:new' - new GroupAvatar() - when 'projects:tree:show' - shortcut_handler = new ShortcutsNavigation() - new TreeView() - when 'projects:find_file:show' - shortcut_handler = true - when 'projects:blob:show', 'projects:blame:show' - new LineHighlighter() - shortcut_handler = new ShortcutsNavigation() - new ShortcutsBlob true - when 'projects:labels:new', 'projects:labels:edit' - new Labels() - when 'projects:labels:index' - new LabelManager() if $('.prioritized-labels').length - when 'projects:network:show' - # Ensure we don't create a particular shortcut handler here. This is - # already created, where the network graph is created. - shortcut_handler = true - when 'projects:forks:new' - new ProjectFork() - when 'projects:artifacts:browse' - new BuildArtifacts() - when 'projects:group_links:index' - new GroupsSelect() - when 'search:show' - new Search() - - switch path.first() - when 'admin' - new Admin() - switch path[1] - when 'groups' - new UsersSelect() - when 'projects' - new NamespaceSelects() - when 'dashboard', 'root' - shortcut_handler = new ShortcutsDashboardNavigation() - when 'profiles' - new NotificationsForm() - new NotificationsDropdown() - when 'projects' - new Project() - new ProjectAvatar() - switch path[1] - when 'compare' - new CompareAutocomplete() - when 'edit' - shortcut_handler = new ShortcutsNavigation() - new ProjectNew() - when 'new' - new ProjectNew() - when 'show' - new ProjectNew() - new ProjectShow() - new NotificationsDropdown() - when 'wikis' - new Wikis() - shortcut_handler = new ShortcutsNavigation() - new ZenMode() - new GLForm($('.wiki-form')) - when 'snippets' - shortcut_handler = new ShortcutsNavigation() - new ZenMode() if path[2] == 'show' - when 'labels', 'graphs', 'compare', 'pipelines', 'forks', \ - 'milestones', 'project_members', 'deploy_keys', 'builds', \ - 'hooks', 'services', 'protected_branches' - shortcut_handler = new ShortcutsNavigation() - - # If we haven't installed a custom shortcut handler, install the default one - if not shortcut_handler - new Shortcuts() - - initSearch: -> - - # Only when search form is present - new SearchAutocomplete() if $('.search').length diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js new file mode 100644 index 00000000000..288cce04f87 --- /dev/null +++ b/app/assets/javascripts/dropzone_input.js @@ -0,0 +1,219 @@ + +/*= require markdown_preview */ + +(function() { + this.DropzoneInput = (function() { + function DropzoneInput(form) { + var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, project_uploads_path, showError, showSpinner, uploadFile, uploadProgress; + Dropzone.autoDiscover = false; + alertClass = "alert alert-danger alert-dismissable div-dropzone-alert"; + alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\""; + divHover = "<div class=\"div-dropzone-hover\"></div>"; + divSpinner = "<div class=\"div-dropzone-spinner\"></div>"; + divAlert = "<div class=\"" + alertClass + "\"></div>"; + iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>"; + iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>"; + uploadProgress = $("<div class=\"div-dropzone-progress\"></div>"); + btnAlert = "<button type=\"button\"" + alertAttr + ">×</button>"; + project_uploads_path = window.project_uploads_path || null; + max_file_size = gon.max_file_size || 10; + form_textarea = $(form).find(".js-gfm-input"); + form_textarea.wrap("<div class=\"div-dropzone\"></div>"); + form_textarea.on('paste', (function(_this) { + return function(event) { + return handlePaste(event); + }; + })(this)); + $mdArea = $(form_textarea).closest('.md-area'); + $(form).setupMarkdownPreview(); + form_dropzone = $(form).find('.div-dropzone'); + form_dropzone.parent().addClass("div-dropzone-wrapper"); + form_dropzone.append(divHover); + form_dropzone.find(".div-dropzone-hover").append(iconPaperclip); + form_dropzone.append(divSpinner); + form_dropzone.find(".div-dropzone-spinner").append(iconSpinner); + form_dropzone.find(".div-dropzone-spinner").append(uploadProgress); + form_dropzone.find(".div-dropzone-spinner").css({ + "opacity": 0, + "display": "none" + }); + dropzone = form_dropzone.dropzone({ + url: project_uploads_path, + dictDefaultMessage: "", + clickable: true, + paramName: "file", + maxFilesize: max_file_size, + uploadMultiple: false, + headers: { + "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + }, + previewContainer: false, + processing: function() { + return $(".div-dropzone-alert").alert("close"); + }, + dragover: function() { + $mdArea.addClass('is-dropzone-hover'); + form.find(".div-dropzone-hover").css("opacity", 0.7); + }, + dragleave: function() { + $mdArea.removeClass('is-dropzone-hover'); + form.find(".div-dropzone-hover").css("opacity", 0); + }, + drop: function() { + $mdArea.removeClass('is-dropzone-hover'); + form.find(".div-dropzone-hover").css("opacity", 0); + form_textarea.focus(); + }, + success: function(header, response) { + pasteText(response.link.markdown); + }, + error: function(temp) { + var checkIfMsgExists, errorAlert; + errorAlert = $(form).find('.error-alert'); + checkIfMsgExists = errorAlert.children().length; + if (checkIfMsgExists === 0) { + errorAlert.append(divAlert); + $(".div-dropzone-alert").append(btnAlert + "Attaching the file failed."); + } + }, + totaluploadprogress: function(totalUploadProgress) { + uploadProgress.text(Math.round(totalUploadProgress) + "%"); + }, + sending: function() { + form_dropzone.find(".div-dropzone-spinner").css({ + "opacity": 0.7, + "display": "inherit" + }); + }, + queuecomplete: function() { + uploadProgress.text(""); + $(".dz-preview").remove(); + $(".markdown-area").trigger("input"); + $(".div-dropzone-spinner").css({ + "opacity": 0, + "display": "none" + }); + } + }); + child = $(dropzone[0]).children("textarea"); + handlePaste = function(event) { + var filename, image, pasteEvent, text; + pasteEvent = event.originalEvent; + if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) { + image = isImage(pasteEvent); + if (image) { + event.preventDefault(); + filename = getFilename(pasteEvent) || "image.png"; + text = "{{" + filename + "}}"; + pasteText(text); + return uploadFile(image.getAsFile(), filename); + } + } + }; + isImage = function(data) { + var i, item; + i = 0; + while (i < data.clipboardData.items.length) { + item = data.clipboardData.items[i]; + if (item.type.indexOf("image") !== -1) { + return item; + } + i++; + } + return false; + }; + pasteText = function(text) { + var afterSelection, beforeSelection, caretEnd, caretStart, textEnd; + caretStart = $(child)[0].selectionStart; + caretEnd = $(child)[0].selectionEnd; + textEnd = $(child).val().length; + beforeSelection = $(child).val().substring(0, caretStart); + afterSelection = $(child).val().substring(caretEnd, textEnd); + $(child).val(beforeSelection + text + afterSelection); + child.get(0).setSelectionRange(caretStart + text.length, caretEnd + text.length); + return form_textarea.trigger("input"); + }; + getFilename = function(e) { + var value; + if (window.clipboardData && window.clipboardData.getData) { + value = window.clipboardData.getData("Text"); + } else if (e.clipboardData && e.clipboardData.getData) { + value = e.clipboardData.getData("text/plain"); + } + value = value.split("\r"); + return value.first(); + }; + uploadFile = function(item, filename) { + var formData; + formData = new FormData(); + formData.append("file", item, filename); + return $.ajax({ + url: project_uploads_path, + type: "POST", + data: formData, + dataType: "json", + processData: false, + contentType: false, + headers: { + "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + }, + beforeSend: function() { + showSpinner(); + return closeAlertMessage(); + }, + success: function(e, textStatus, response) { + return insertToTextArea(filename, response.responseJSON.link.markdown); + }, + error: function(response) { + return showError(response.responseJSON.message); + }, + complete: function() { + return closeSpinner(); + } + }); + }; + insertToTextArea = function(filename, url) { + return $(child).val(function(index, val) { + return val.replace("{{" + filename + "}}", url + "\n"); + }); + }; + appendToTextArea = function(url) { + return $(child).val(function(index, val) { + return val + url + "\n"; + }); + }; + showSpinner = function(e) { + return form.find(".div-dropzone-spinner").css({ + "opacity": 0.7, + "display": "inherit" + }); + }; + closeSpinner = function() { + return form.find(".div-dropzone-spinner").css({ + "opacity": 0, + "display": "none" + }); + }; + showError = function(message) { + var checkIfMsgExists, errorAlert; + errorAlert = $(form).find('.error-alert'); + checkIfMsgExists = errorAlert.children().length; + if (checkIfMsgExists === 0) { + errorAlert.append(divAlert); + return $(".div-dropzone-alert").append(btnAlert + message); + } + }; + closeAlertMessage = function() { + return form.find(".div-dropzone-alert").alert("close"); + }; + form.find(".markdown-selector").click(function(e) { + e.preventDefault(); + $(this).closest('.gfm-form').find('.div-dropzone').click(); + }); + } + + return DropzoneInput; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee deleted file mode 100644 index 665246e2a7d..00000000000 --- a/app/assets/javascripts/dropzone_input.js.coffee +++ /dev/null @@ -1,201 +0,0 @@ -#= require markdown_preview - -class @DropzoneInput - constructor: (form) -> - Dropzone.autoDiscover = false - alertClass = "alert alert-danger alert-dismissable div-dropzone-alert" - alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"" - divHover = "<div class=\"div-dropzone-hover\"></div>" - divSpinner = "<div class=\"div-dropzone-spinner\"></div>" - divAlert = "<div class=\"" + alertClass + "\"></div>" - iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>" - iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>" - uploadProgress = $("<div class=\"div-dropzone-progress\"></div>") - btnAlert = "<button type=\"button\"" + alertAttr + ">×</button>" - project_uploads_path = window.project_uploads_path or null - max_file_size = gon.max_file_size or 10 - - form_textarea = $(form).find(".js-gfm-input") - form_textarea.wrap "<div class=\"div-dropzone\"></div>" - form_textarea.on 'paste', (event) => - handlePaste(event) - - $mdArea = $(form_textarea).closest('.md-area') - - $(form).setupMarkdownPreview() - - form_dropzone = $(form).find('.div-dropzone') - form_dropzone.parent().addClass "div-dropzone-wrapper" - form_dropzone.append divHover - form_dropzone.find(".div-dropzone-hover").append iconPaperclip - form_dropzone.append divSpinner - form_dropzone.find(".div-dropzone-spinner").append iconSpinner - form_dropzone.find(".div-dropzone-spinner").append uploadProgress - form_dropzone.find(".div-dropzone-spinner").css - "opacity": 0 - "display": "none" - - dropzone = form_dropzone.dropzone( - url: project_uploads_path - dictDefaultMessage: "" - clickable: true - paramName: "file" - maxFilesize: max_file_size - uploadMultiple: false - headers: - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") - - previewContainer: false - - processing: -> - $(".div-dropzone-alert").alert "close" - - dragover: -> - $mdArea.addClass 'is-dropzone-hover' - form.find(".div-dropzone-hover").css "opacity", 0.7 - return - - dragleave: -> - $mdArea.removeClass 'is-dropzone-hover' - form.find(".div-dropzone-hover").css "opacity", 0 - return - - drop: -> - $mdArea.removeClass 'is-dropzone-hover' - form.find(".div-dropzone-hover").css "opacity", 0 - form_textarea.focus() - return - - success: (header, response) -> - pasteText response.link.markdown - return - - error: (temp) -> - errorAlert = $(form).find('.error-alert') - checkIfMsgExists = errorAlert.children().length - if checkIfMsgExists is 0 - errorAlert.append divAlert - $(".div-dropzone-alert").append "#{btnAlert}Attaching the file failed." - return - - totaluploadprogress: (totalUploadProgress) -> - uploadProgress.text Math.round(totalUploadProgress) + "%" - return - - sending: -> - form_dropzone.find(".div-dropzone-spinner").css - "opacity": 0.7 - "display": "inherit" - return - - queuecomplete: -> - uploadProgress.text "" - $(".dz-preview").remove() - $(".markdown-area").trigger "input" - $(".div-dropzone-spinner").css - "opacity": 0 - "display": "none" - return - ) - - child = $(dropzone[0]).children("textarea") - - handlePaste = (event) -> - pasteEvent = event.originalEvent - if pasteEvent.clipboardData and pasteEvent.clipboardData.items - image = isImage(pasteEvent) - if image - event.preventDefault() - - filename = getFilename(pasteEvent) or "image.png" - text = "{{" + filename + "}}" - pasteText(text) - uploadFile image.getAsFile(), filename - - isImage = (data) -> - i = 0 - while i < data.clipboardData.items.length - item = data.clipboardData.items[i] - if item.type.indexOf("image") isnt -1 - return item - i++ - return false - - pasteText = (text) -> - caretStart = $(child)[0].selectionStart - caretEnd = $(child)[0].selectionEnd - textEnd = $(child).val().length - - beforeSelection = $(child).val().substring 0, caretStart - afterSelection = $(child).val().substring caretEnd, textEnd - $(child).val beforeSelection + text + afterSelection - child.get(0).setSelectionRange caretStart + text.length, caretEnd + text.length - form_textarea.trigger "input" - - getFilename = (e) -> - if window.clipboardData and window.clipboardData.getData - value = window.clipboardData.getData("Text") - else if e.clipboardData and e.clipboardData.getData - value = e.clipboardData.getData("text/plain") - - value = value.split("\r") - value.first() - - uploadFile = (item, filename) -> - formData = new FormData() - formData.append "file", item, filename - $.ajax - url: project_uploads_path - type: "POST" - data: formData - dataType: "json" - processData: false - contentType: false - headers: - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") - - beforeSend: -> - showSpinner() - closeAlertMessage() - - success: (e, textStatus, response) -> - insertToTextArea(filename, response.responseJSON.link.markdown) - - error: (response) -> - showError(response.responseJSON.message) - - complete: -> - closeSpinner() - - insertToTextArea = (filename, url) -> - $(child).val (index, val) -> - val.replace("{{" + filename + "}}", url + "\n") - - appendToTextArea = (url) -> - $(child).val (index, val) -> - val + url + "\n" - - showSpinner = (e) -> - form.find(".div-dropzone-spinner").css - "opacity": 0.7 - "display": "inherit" - - closeSpinner = -> - form.find(".div-dropzone-spinner").css - "opacity": 0 - "display": "none" - - showError = (message) -> - errorAlert = $(form).find('.error-alert') - checkIfMsgExists = errorAlert.children().length - if checkIfMsgExists is 0 - errorAlert.append divAlert - $(".div-dropzone-alert").append btnAlert + message - - closeAlertMessage = -> - form.find(".div-dropzone-alert").alert "close" - - form.find(".markdown-selector").click (e) -> - e.preventDefault() - $(@).closest('.gfm-form').find('.div-dropzone').click() - return diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js new file mode 100644 index 00000000000..5a725a41fd1 --- /dev/null +++ b/app/assets/javascripts/due_date_select.js @@ -0,0 +1,104 @@ +(function() { + this.DueDateSelect = (function() { + function DueDateSelect() { + var $datePicker, $dueDate, $loading; + $datePicker = $('.datepicker'); + if ($datePicker.length) { + $dueDate = $('#milestone_due_date'); + $datePicker.datepicker({ + dateFormat: 'yy-mm-dd', + onSelect: function(dateText, inst) { + return $dueDate.val(dateText); + } + }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val())); + } + $('.js-clear-due-date').on('click', function(e) { + e.preventDefault(); + return $.datepicker._clearDate($datePicker); + }); + $loading = $('.js-issuable-update .due_date').find('.block-loading').hide(); + $('.js-due-date-select').each(function(i, dropdown) { + var $block, $dropdown, $dropdownParent, $selectbox, $sidebarValue, $value, $valueContent, abilityName, addDueDate, fieldName, issueUpdateURL; + $dropdown = $(dropdown); + $dropdownParent = $dropdown.closest('.dropdown'); + $datePicker = $dropdownParent.find('.js-due-date-calendar'); + $block = $dropdown.closest('.block'); + $selectbox = $dropdown.closest('.selectbox'); + $value = $block.find('.value'); + $valueContent = $block.find('.value-content'); + $sidebarValue = $('.js-due-date-sidebar-value', $block); + fieldName = $dropdown.data('field-name'); + abilityName = $dropdown.data('ability-name'); + issueUpdateURL = $dropdown.data('issue-update'); + $dropdown.glDropdown({ + hidden: function() { + $selectbox.hide(); + return $value.css('display', ''); + } + }); + addDueDate = function(isDropdown) { + var data, date, mediumDate, value; + value = $("input[name='" + fieldName + "']").val(); + if (value !== '') { + date = new Date(value.replace(new RegExp('-', 'g'), ',')); + mediumDate = $.datepicker.formatDate('M d, yy', date); + } else { + mediumDate = 'No due date'; + } + data = {}; + data[abilityName] = {}; + data[abilityName].due_date = value; + return $.ajax({ + type: 'PUT', + url: issueUpdateURL, + data: data, + dataType: 'json', + beforeSend: function() { + var cssClass; + $loading.fadeIn(); + if (isDropdown) { + $dropdown.trigger('loading.gl.dropdown'); + $selectbox.hide(); + } + $value.css('display', ''); + cssClass = Date.parse(mediumDate) ? 'bold' : 'no-value'; + $valueContent.html("<span class='" + cssClass + "'>" + mediumDate + "</span>"); + $sidebarValue.html(mediumDate); + if (value !== '') { + return $('.js-remove-due-date-holder').removeClass('hidden'); + } else { + return $('.js-remove-due-date-holder').addClass('hidden'); + } + } + }).done(function(data) { + if (isDropdown) { + $dropdown.trigger('loaded.gl.dropdown'); + $dropdown.dropdown('toggle'); + } + return $loading.fadeOut(); + }); + }; + $block.on('click', '.js-remove-due-date', function(e) { + e.preventDefault(); + $("input[name='" + fieldName + "']").val(''); + return addDueDate(false); + }); + return $datePicker.datepicker({ + dateFormat: 'yy-mm-dd', + defaultDate: $("input[name='" + fieldName + "']").val(), + altField: "input[name='" + fieldName + "']", + onSelect: function() { + return addDueDate(true); + } + }); + }); + $(document).off('click', '.ui-datepicker-header a').on('click', '.ui-datepicker-header a', function(e) { + return e.stopImmediatePropagation(); + }); + } + + return DueDateSelect; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee deleted file mode 100644 index d65c018dad5..00000000000 --- a/app/assets/javascripts/due_date_select.js.coffee +++ /dev/null @@ -1,99 +0,0 @@ -class @DueDateSelect - constructor: -> - # Milestone edit/new form - $datePicker = $('.datepicker') - - if $datePicker.length - $dueDate = $('#milestone_due_date') - $datePicker.datepicker - dateFormat: 'yy-mm-dd' - onSelect: (dateText, inst) -> - $dueDate.val(dateText) - .datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val())) - - $('.js-clear-due-date').on 'click', (e) -> - e.preventDefault() - $.datepicker._clearDate($datePicker) - - # Issuable sidebar - $loading = $('.js-issuable-update .due_date') - .find('.block-loading') - .hide() - - $('.js-due-date-select').each (i, dropdown) -> - $dropdown = $(dropdown) - $dropdownParent = $dropdown.closest('.dropdown') - $datePicker = $dropdownParent.find('.js-due-date-calendar') - $block = $dropdown.closest('.block') - $selectbox = $dropdown.closest('.selectbox') - $value = $block.find('.value') - $valueContent = $block.find('.value-content') - $sidebarValue = $('.js-due-date-sidebar-value', $block) - - fieldName = $dropdown.data('field-name') - abilityName = $dropdown.data('ability-name') - issueUpdateURL = $dropdown.data('issue-update') - - $dropdown.glDropdown( - hidden: -> - $selectbox.hide() - $value.css('display', '') - ) - - addDueDate = (isDropdown) -> - # Create the post date - value = $("input[name='#{fieldName}']").val() - - if value isnt '' - date = new Date value.replace(new RegExp('-', 'g'), ',') - mediumDate = $.datepicker.formatDate 'M d, yy', date - else - mediumDate = 'No due date' - - data = {} - data[abilityName] = {} - data[abilityName].due_date = value - - $.ajax( - type: 'PUT' - url: issueUpdateURL - data: data - dataType: 'json' - beforeSend: -> - $loading.fadeIn() - if isDropdown - $dropdown.trigger('loading.gl.dropdown') - $selectbox.hide() - $value.css('display', '') - - cssClass = if Date.parse(mediumDate) then 'bold' else 'no-value' - $valueContent.html("<span class='#{cssClass}'>#{mediumDate}</span>") - $sidebarValue.html(mediumDate) - - if value isnt '' - $('.js-remove-due-date-holder').removeClass 'hidden' - else - $('.js-remove-due-date-holder').addClass 'hidden' - ).done (data) -> - if isDropdown - $dropdown.trigger('loaded.gl.dropdown') - $dropdown.dropdown('toggle') - $loading.fadeOut() - - $block.on 'click', '.js-remove-due-date', (e) -> - e.preventDefault() - $("input[name='#{fieldName}']").val '' - addDueDate(false) - - $datePicker.datepicker( - dateFormat: 'yy-mm-dd', - defaultDate: $("input[name='#{fieldName}']").val() - altField: "input[name='#{fieldName}']" - onSelect: -> - addDueDate(true) - ) - - $(document) - .off 'click', '.ui-datepicker-header a' - .on 'click', '.ui-datepicker-header a', (e) -> - e.stopImmediatePropagation() diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js new file mode 100644 index 00000000000..ae3dde63da3 --- /dev/null +++ b/app/assets/javascripts/extensions/jquery.js @@ -0,0 +1,14 @@ +(function() { + $.fn.extend({ + disable: function() { + return $(this).attr('disabled', 'disabled').addClass('disabled'); + } + }); + + $.fn.extend({ + enable: function() { + return $(this).removeAttr('disabled').removeClass('disabled'); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/extensions/jquery.js.coffee b/app/assets/javascripts/extensions/jquery.js.coffee deleted file mode 100644 index 0a9db8eb5ef..00000000000 --- a/app/assets/javascripts/extensions/jquery.js.coffee +++ /dev/null @@ -1,11 +0,0 @@ -# Disable an element and add the 'disabled' Bootstrap class -$.fn.extend disable: -> - $(@) - .attr('disabled', 'disabled') - .addClass('disabled') - -# Enable an element and remove the 'disabled' Bootstrap class -$.fn.extend enable: -> - $(@) - .removeAttr('disabled') - .removeClass('disabled') diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js new file mode 100644 index 00000000000..09b5eb398d4 --- /dev/null +++ b/app/assets/javascripts/files_comment_button.js @@ -0,0 +1,141 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.FilesCommentButton = (function() { + var COMMENT_BUTTON_CLASS, COMMENT_BUTTON_TEMPLATE, DEBOUNCE_TIMEOUT_DURATION, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; + + COMMENT_BUTTON_CLASS = '.add-diff-note'; + + COMMENT_BUTTON_TEMPLATE = _.template('<button name="button" type="submit" class="btn <%- COMMENT_BUTTON_CLASS %> js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>'); + + LINE_HOLDER_CLASS = '.line_holder'; + + LINE_NUMBER_CLASS = 'diff-line-num'; + + LINE_CONTENT_CLASS = 'line_content'; + + UNFOLDABLE_LINE_CLASS = 'js-unfold'; + + EMPTY_CELL_CLASS = 'empty-cell'; + + OLD_LINE_CLASS = 'old_line'; + + LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content"; + + TEXT_FILE_SELECTOR = '.text-file'; + + DEBOUNCE_TIMEOUT_DURATION = 100; + + function FilesCommentButton(filesContainerElement) { + var debounce; + this.filesContainerElement = filesContainerElement; + this.destroy = bind(this.destroy, this); + this.render = bind(this.render, this); + this.VIEW_TYPE = $('input#view[type=hidden]').val(); + debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION); + $(document).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy); + } + + FilesCommentButton.prototype.render = function(e) { + var $currentTarget, buttonParentElement, lineContentElement, textFileElement; + $currentTarget = $(e.currentTarget); + buttonParentElement = this.getButtonParent($currentTarget); + if (!this.shouldRender(e, buttonParentElement)) { + return; + } + textFileElement = this.getTextFileElement($currentTarget); + lineContentElement = this.getLineContent($currentTarget); + buttonParentElement.append(this.buildButton({ + noteableType: textFileElement.attr('data-noteable-type'), + noteableID: textFileElement.attr('data-noteable-id'), + commitID: textFileElement.attr('data-commit-id'), + noteType: lineContentElement.attr('data-note-type'), + position: lineContentElement.attr('data-position'), + lineType: lineContentElement.attr('data-line-type'), + discussionID: lineContentElement.attr('data-discussion-id'), + lineCode: lineContentElement.attr('data-line-code') + })); + }; + + FilesCommentButton.prototype.destroy = function(e) { + if (this.isMovingToSameType(e)) { + return; + } + $(COMMENT_BUTTON_CLASS, this.getButtonParent($(e.currentTarget))).remove(); + }; + + FilesCommentButton.prototype.buildButton = function(buttonAttributes) { + var initializedButtonTemplate; + initializedButtonTemplate = COMMENT_BUTTON_TEMPLATE({ + COMMENT_BUTTON_CLASS: COMMENT_BUTTON_CLASS.substr(1) + }); + return $(initializedButtonTemplate).attr({ + 'data-noteable-type': buttonAttributes.noteableType, + 'data-noteable-id': buttonAttributes.noteableID, + 'data-commit-id': buttonAttributes.commitID, + 'data-note-type': buttonAttributes.noteType, + 'data-line-code': buttonAttributes.lineCode, + 'data-position': buttonAttributes.position, + 'data-discussion-id': buttonAttributes.discussionID, + 'data-line-type': buttonAttributes.lineType + }); + }; + + FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) { + return $(hoveredElement.closest(TEXT_FILE_SELECTOR)); + }; + + FilesCommentButton.prototype.getLineContent = function(hoveredElement) { + if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) { + return hoveredElement; + } + if (this.VIEW_TYPE === 'inline') { + return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS); + } else { + return $(hoveredElement).next("." + LINE_CONTENT_CLASS); + } + }; + + FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { + if (this.VIEW_TYPE === 'inline') { + if (hoveredElement.hasClass(OLD_LINE_CLASS)) { + return hoveredElement; + } + return hoveredElement.parent().find("." + OLD_LINE_CLASS); + } else { + if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) { + return hoveredElement; + } + return $(hoveredElement).prev("." + LINE_NUMBER_CLASS); + } + }; + + FilesCommentButton.prototype.isMovingToSameType = function(e) { + var newButtonParent; + newButtonParent = this.getButtonParent($(e.toElement)); + if (!newButtonParent) { + return false; + } + return newButtonParent.is(this.getButtonParent($(e.currentTarget))); + }; + + FilesCommentButton.prototype.shouldRender = function(e, buttonParentElement) { + return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS) && $(COMMENT_BUTTON_CLASS, buttonParentElement).length === 0; + }; + + return FilesCommentButton; + + })(); + + $.fn.filesCommentButton = function() { + if (!(this && (this.parent().data('can-create-note') != null))) { + return; + } + return this.each(function() { + if (!$.data(this, 'filesCommentButton')) { + return $.data(this, 'filesCommentButton', new FilesCommentButton($(this))); + } + }); + }; + +}).call(this); diff --git a/app/assets/javascripts/files_comment_button.js.coffee b/app/assets/javascripts/files_comment_button.js.coffee deleted file mode 100644 index 5ab82c39fcd..00000000000 --- a/app/assets/javascripts/files_comment_button.js.coffee +++ /dev/null @@ -1,98 +0,0 @@ -class @FilesCommentButton - COMMENT_BUTTON_CLASS = '.add-diff-note' - COMMENT_BUTTON_TEMPLATE = _.template '<button name="button" type="submit" class="btn <%- COMMENT_BUTTON_CLASS %> js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>' - LINE_HOLDER_CLASS = '.line_holder' - LINE_NUMBER_CLASS = 'diff-line-num' - LINE_CONTENT_CLASS = 'line_content' - UNFOLDABLE_LINE_CLASS = 'js-unfold' - EMPTY_CELL_CLASS = 'empty-cell' - OLD_LINE_CLASS = 'old_line' - LINE_COLUMN_CLASSES = ".#{LINE_NUMBER_CLASS}, .line_content" - TEXT_FILE_SELECTOR = '.text-file' - DEBOUNCE_TIMEOUT_DURATION = 100 - - constructor: (@filesContainerElement) -> - @VIEW_TYPE = $('input#view[type=hidden]').val() - - debounce = _.debounce @render, DEBOUNCE_TIMEOUT_DURATION - - $(document) - .off 'mouseover', LINE_COLUMN_CLASSES - .off 'mouseleave', LINE_COLUMN_CLASSES - .on 'mouseover', LINE_COLUMN_CLASSES, debounce - .on 'mouseleave', LINE_COLUMN_CLASSES, @destroy - - render: (e) => - $currentTarget = $(e.currentTarget) - buttonParentElement = @getButtonParent $currentTarget - return unless @shouldRender e, buttonParentElement - - textFileElement = @getTextFileElement $currentTarget - lineContentElement = @getLineContent $currentTarget - - buttonParentElement.append @buildButton - noteableType: textFileElement.attr 'data-noteable-type' - noteableID: textFileElement.attr 'data-noteable-id' - commitID: textFileElement.attr 'data-commit-id' - noteType: lineContentElement.attr 'data-note-type' - position: lineContentElement.attr 'data-position' - lineType: lineContentElement.attr 'data-line-type' - discussionID: lineContentElement.attr 'data-discussion-id' - lineCode: lineContentElement.attr 'data-line-code' - return - - destroy: (e) => - return if @isMovingToSameType e - $(COMMENT_BUTTON_CLASS, @getButtonParent $(e.currentTarget)).remove() - return - - buildButton: (buttonAttributes) -> - initializedButtonTemplate = COMMENT_BUTTON_TEMPLATE - COMMENT_BUTTON_CLASS: COMMENT_BUTTON_CLASS.substr 1 - $(initializedButtonTemplate).attr - 'data-noteable-type': buttonAttributes.noteableType - 'data-noteable-id': buttonAttributes.noteableID - 'data-commit-id': buttonAttributes.commitID - 'data-note-type': buttonAttributes.noteType - 'data-line-code': buttonAttributes.lineCode - 'data-position': buttonAttributes.position - 'data-discussion-id': buttonAttributes.discussionID - 'data-line-type': buttonAttributes.lineType - - getTextFileElement: (hoveredElement) -> - $(hoveredElement.closest TEXT_FILE_SELECTOR) - - getLineContent: (hoveredElement) -> - return hoveredElement if hoveredElement.hasClass LINE_CONTENT_CLASS - - if @VIEW_TYPE is 'inline' - return $(hoveredElement).closest(LINE_HOLDER_CLASS).find ".#{LINE_CONTENT_CLASS}" - else - return $(hoveredElement).next ".#{LINE_CONTENT_CLASS}" - - getButtonParent: (hoveredElement) -> - if @VIEW_TYPE is 'inline' - return hoveredElement if hoveredElement.hasClass OLD_LINE_CLASS - - hoveredElement.parent().find ".#{OLD_LINE_CLASS}" - else - return hoveredElement if hoveredElement.hasClass LINE_NUMBER_CLASS - - $(hoveredElement).prev ".#{LINE_NUMBER_CLASS}" - - isMovingToSameType: (e) -> - newButtonParent = @getButtonParent $(e.toElement) - return false unless newButtonParent - newButtonParent.is @getButtonParent $(e.currentTarget) - - shouldRender: (e, buttonParentElement) -> - (not buttonParentElement.hasClass(EMPTY_CELL_CLASS) and \ - not buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS) and \ - $(COMMENT_BUTTON_CLASS, buttonParentElement).length is 0) - -$.fn.filesCommentButton = -> - return unless this and @parent().data('can-create-note')? - - @each -> - unless $.data this, 'filesCommentButton' - $.data this, 'filesCommentButton', new FilesCommentButton $(this) diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js new file mode 100644 index 00000000000..c8a02d6fa15 --- /dev/null +++ b/app/assets/javascripts/flash.js @@ -0,0 +1,43 @@ +(function() { + this.Flash = (function() { + var hideFlash; + + hideFlash = function() { + return $(this).fadeOut(); + }; + + function Flash(message, type, parent) { + var flash, textDiv; + if (type == null) { + type = 'alert'; + } + if (parent == null) { + parent = null; + } + if (parent) { + this.flashContainer = parent.find('.flash-container'); + } else { + this.flashContainer = $('.flash-container-page'); + } + this.flashContainer.html(''); + flash = $('<div/>', { + "class": "flash-" + type + }); + flash.on('click', hideFlash); + textDiv = $('<div/>', { + "class": 'flash-text', + text: message + }); + textDiv.appendTo(flash); + if (this.flashContainer.parent().hasClass('content-wrapper')) { + textDiv.addClass('container-fluid container-limited'); + } + flash.appendTo(this.flashContainer); + this.flashContainer.show(); + } + + return Flash; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee deleted file mode 100644 index 5a493041538..00000000000 --- a/app/assets/javascripts/flash.js.coffee +++ /dev/null @@ -1,28 +0,0 @@ -class @Flash - hideFlash = -> $(@).fadeOut() - - constructor: (message, type = 'alert', parent = null)-> - if parent - @flashContainer = parent.find('.flash-container') - else - @flashContainer = $('.flash-container-page') - - @flashContainer.html('') - - flash = $('<div/>', - class: "flash-#{type}" - ) - flash.on 'click', hideFlash - - textDiv = $('<div/>', - class: 'flash-text', - text: message - ) - textDiv.appendTo(flash) - - if @flashContainer.parent().hasClass('content-wrapper') - textDiv.addClass('container-fluid container-limited') - - flash.appendTo(@flashContainer) - @flashContainer.show() - diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js new file mode 100644 index 00000000000..41f4c1914f2 --- /dev/null +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -0,0 +1,272 @@ +(function() { + if (window.GitLab == null) { + window.GitLab = {}; + } + + GitLab.GfmAutoComplete = { + dataLoading: false, + dataLoaded: false, + cachedData: {}, + dataSource: '', + Emoji: { + template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>' + }, + Members: { + template: '<li>${username} <small>${title}</small></li>' + }, + Labels: { + template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>' + }, + Issues: { + template: '<li><small>${id}</small> ${title}</li>' + }, + Milestones: { + template: '<li>${title}</li>' + }, + Loading: { + template: '<li><i class="fa fa-refresh fa-spin"></i> Loading...</li>' + }, + DefaultOptions: { + sorter: function(query, items, searchKey) { + if ((items[0].name != null) && items[0].name === 'loading') { + return items; + } + return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey); + }, + filter: function(query, data, searchKey) { + if (data[0] === 'loading') { + return data; + } + return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); + }, + beforeInsert: function(value) { + if (!GitLab.GfmAutoComplete.dataLoaded) { + return this.at; + } else { + return value; + } + } + }, + setup: function(wrap) { + this.input = $('.js-gfm-input'); + this.destroyAtWho(); + this.setupAtWho(); + if (this.dataSource) { + if (!this.dataLoading && !this.cachedData) { + this.dataLoading = true; + setTimeout((function(_this) { + return function() { + var fetch; + fetch = _this.fetchData(_this.dataSource); + return fetch.done(function(data) { + _this.dataLoading = false; + return _this.loadData(data); + }); + }; + })(this), 1000); + } + if (this.cachedData != null) { + return this.loadData(this.cachedData); + } + } + }, + setupAtWho: function() { + this.input.atwho({ + at: ':', + displayTpl: (function(_this) { + return function(value) { + if (value.path != null) { + return _this.Emoji.template; + } else { + return _this.Loading.template; + } + }; + })(this), + insertTpl: ':${name}:', + data: ['loading'], + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert + } + }); + this.input.atwho({ + at: '@', + displayTpl: (function(_this) { + return function(value) { + if (value.username != null) { + return _this.Members.template; + } else { + return _this.Loading.template; + } + }; + })(this), + insertTpl: '${atwho-at}${username}', + searchKey: 'search', + data: ['loading'], + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + beforeSave: function(members) { + return $.map(members, function(m) { + var title; + if (m.username == null) { + return m; + } + title = m.name; + if (m.count) { + title += " (" + m.count + ")"; + } + return { + username: m.username, + title: sanitize(title), + search: sanitize(m.username + " " + m.name) + }; + }); + } + } + }); + this.input.atwho({ + at: '#', + alias: 'issues', + searchKey: 'search', + displayTpl: (function(_this) { + return function(value) { + if (value.title != null) { + return _this.Issues.template; + } else { + return _this.Loading.template; + } + }; + })(this), + data: ['loading'], + insertTpl: '${atwho-at}${id}', + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + beforeSave: function(issues) { + return $.map(issues, function(i) { + if (i.title == null) { + return i; + } + return { + id: i.iid, + title: sanitize(i.title), + search: i.iid + " " + i.title + }; + }); + } + } + }); + this.input.atwho({ + at: '%', + alias: 'milestones', + searchKey: 'search', + displayTpl: (function(_this) { + return function(value) { + if (value.title != null) { + return _this.Milestones.template; + } else { + return _this.Loading.template; + } + }; + })(this), + insertTpl: '${atwho-at}"${title}"', + data: ['loading'], + callbacks: { + beforeSave: function(milestones) { + return $.map(milestones, function(m) { + if (m.title == null) { + return m; + } + return { + id: m.iid, + title: sanitize(m.title), + search: "" + m.title + }; + }); + } + } + }); + this.input.atwho({ + at: '!', + alias: 'mergerequests', + searchKey: 'search', + displayTpl: (function(_this) { + return function(value) { + if (value.title != null) { + return _this.Issues.template; + } else { + return _this.Loading.template; + } + }; + })(this), + data: ['loading'], + insertTpl: '${atwho-at}${id}', + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + beforeSave: function(merges) { + return $.map(merges, function(m) { + if (m.title == null) { + return m; + } + return { + id: m.iid, + title: sanitize(m.title), + search: m.iid + " " + m.title + }; + }); + } + } + }); + return this.input.atwho({ + at: '~', + alias: 'labels', + searchKey: 'search', + displayTpl: this.Labels.template, + insertTpl: '${atwho-at}${title}', + callbacks: { + beforeSave: function(merges) { + var sanitizeLabelTitle; + sanitizeLabelTitle = function(title) { + if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) { + return "\"" + (sanitize(title)) + "\""; + } else { + return sanitize(title); + } + }; + return $.map(merges, function(m) { + return { + title: sanitizeLabelTitle(m.title), + color: m.color, + search: "" + m.title + }; + }); + } + } + }); + }, + destroyAtWho: function() { + return this.input.atwho('destroy'); + }, + fetchData: function(dataSource) { + return $.getJSON(dataSource); + }, + loadData: function(data) { + this.cachedData = data; + this.dataLoaded = true; + this.input.atwho('load', '@', data.members); + this.input.atwho('load', 'issues', data.issues); + this.input.atwho('load', 'milestones', data.milestones); + this.input.atwho('load', 'mergerequests', data.mergerequests); + this.input.atwho('load', ':', data.emojis); + this.input.atwho('load', '~', data.labels); + return $(':focus').trigger('keyup'); + } + }; + +}).call(this); diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee deleted file mode 100644 index 4a851d9c9fb..00000000000 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ /dev/null @@ -1,228 +0,0 @@ -# Creates the variables for setting up GFM auto-completion - -window.GitLab ?= {} -GitLab.GfmAutoComplete = - dataLoading: false - dataLoaded: false - cachedData: {} - dataSource: '' - - # Emoji - Emoji: - template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>' - - # Team Members - Members: - template: '<li>${username} <small>${title}</small></li>' - - Labels: - template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>' - - # Issues and MergeRequests - Issues: - template: '<li><small>${id}</small> ${title}</li>' - - # Milestones - Milestones: - template: '<li>${title}</li>' - - Loading: - template: '<li><i class="fa fa-refresh fa-spin"></i> Loading...</li>' - - DefaultOptions: - sorter: (query, items, searchKey) -> - return items if items[0].name? and items[0].name is 'loading' - - $.fn.atwho.default.callbacks.sorter(query, items, searchKey) - filter: (query, data, searchKey) -> - return data if data[0] is 'loading' - - $.fn.atwho.default.callbacks.filter(query, data, searchKey) - beforeInsert: (value) -> - if not GitLab.GfmAutoComplete.dataLoaded - @at - else - value - - # Add GFM auto-completion to all input fields, that accept GFM input. - setup: (wrap) -> - @input = $('.js-gfm-input') - - # destroy previous instances - @destroyAtWho() - - # set up instances - @setupAtWho() - - if @dataSource - if not @dataLoading and not @cachedData - @dataLoading = true - - # We should wait until initializations are done - # and only trigger the last .setup since - # The previous .dataSource belongs to the previous issuable - # and the last one will have the **proper** .dataSource property - # TODO: Make this a singleton and turn off events when moving to another page - setTimeout( => - fetch = @fetchData(@dataSource) - fetch.done (data) => - @dataLoading = false - @loadData(data) - , 1000) - - if @cachedData? - @loadData(@cachedData) - - setupAtWho: -> - # Emoji - @input.atwho - at: ':' - displayTpl: (value) => - if value.path? - @Emoji.template - else - @Loading.template - insertTpl: ':${name}:' - data: ['loading'] - callbacks: - sorter: @DefaultOptions.sorter - filter: @DefaultOptions.filter - beforeInsert: @DefaultOptions.beforeInsert - - # Team Members - @input.atwho - at: '@' - displayTpl: (value) => - if value.username? - @Members.template - else - @Loading.template - insertTpl: '${atwho-at}${username}' - searchKey: 'search' - data: ['loading'] - callbacks: - sorter: @DefaultOptions.sorter - filter: @DefaultOptions.filter - beforeInsert: @DefaultOptions.beforeInsert - beforeSave: (members) -> - $.map members, (m) -> - return m if not m.username? - - title = m.name - title += " (#{m.count})" if m.count - - username: m.username - title: sanitize(title) - search: sanitize("#{m.username} #{m.name}") - - @input.atwho - at: '#' - alias: 'issues' - searchKey: 'search' - displayTpl: (value) => - if value.title? - @Issues.template - else - @Loading.template - data: ['loading'] - insertTpl: '${atwho-at}${id}' - callbacks: - sorter: @DefaultOptions.sorter - filter: @DefaultOptions.filter - beforeInsert: @DefaultOptions.beforeInsert - beforeSave: (issues) -> - $.map issues, (i) -> - return i if not i.title? - - id: i.iid - title: sanitize(i.title) - search: "#{i.iid} #{i.title}" - - @input.atwho - at: '%' - alias: 'milestones' - searchKey: 'search' - displayTpl: (value) => - if value.title? - @Milestones.template - else - @Loading.template - insertTpl: '${atwho-at}"${title}"' - data: ['loading'] - callbacks: - beforeSave: (milestones) -> - $.map milestones, (m) -> - return m if not m.title? - - id: m.iid - title: sanitize(m.title) - search: "#{m.title}" - - @input.atwho - at: '!' - alias: 'mergerequests' - searchKey: 'search' - displayTpl: (value) => - if value.title? - @Issues.template - else - @Loading.template - data: ['loading'] - insertTpl: '${atwho-at}${id}' - callbacks: - sorter: @DefaultOptions.sorter - filter: @DefaultOptions.filter - beforeInsert: @DefaultOptions.beforeInsert - beforeSave: (merges) -> - $.map merges, (m) -> - return m if not m.title? - - id: m.iid - title: sanitize(m.title) - search: "#{m.iid} #{m.title}" - - @input.atwho - at: '~' - alias: 'labels' - searchKey: 'search' - displayTpl: @Labels.template - insertTpl: '${atwho-at}${title}' - callbacks: - beforeSave: (merges) -> - sanitizeLabelTitle = (title)-> - if /[\w\?&]+\s+[\w\?&]+/g.test(title) - "\"#{sanitize(title)}\"" - else - sanitize(title) - - $.map merges, (m) -> - title: sanitizeLabelTitle(m.title) - color: m.color - search: "#{m.title}" - - destroyAtWho: -> - @input.atwho('destroy') - - fetchData: (dataSource) -> - $.getJSON(dataSource) - - loadData: (data) -> - @cachedData = data - @dataLoaded = true - - # load members - @input.atwho 'load', '@', data.members - # load issues - @input.atwho 'load', 'issues', data.issues - # load milestones - @input.atwho 'load', 'milestones', data.milestones - # load merge requests - @input.atwho 'load', 'mergerequests', data.mergerequests - # load emojis - @input.atwho 'load', ':', data.emojis - # load labels - @input.atwho 'load', '~', data.labels - - # This trigger at.js again - # otherwise we would be stuck with loading until the user types - $(':focus').trigger('keyup') diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js new file mode 100644 index 00000000000..c5d92831fbe --- /dev/null +++ b/app/assets/javascripts/gl_dropdown.js @@ -0,0 +1,705 @@ +(function() { + var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, + bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + + GitLabDropdownFilter = (function() { + var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS; + + BLUR_KEYCODES = [27, 40]; + + ARROW_KEY_CODES = [38, 40]; + + HAS_VALUE_CLASS = "has-value"; + + function GitLabDropdownFilter(input, options) { + var $clearButton, $inputContainer, ref, timeout; + this.input = input; + this.options = options; + this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true; + $inputContainer = this.input.parent(); + $clearButton = $inputContainer.find('.js-dropdown-input-clear'); + this.indeterminateIds = []; + $clearButton.on('click', (function(_this) { + return function(e) { + e.preventDefault(); + e.stopPropagation(); + return _this.input.val('').trigger('keyup').focus(); + }; + })(this)); + timeout = ""; + this.input.on("keyup", (function(_this) { + return function(e) { + var keyCode; + keyCode = e.which; + if (ARROW_KEY_CODES.indexOf(keyCode) >= 0) { + return; + } + if (_this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) { + $inputContainer.addClass(HAS_VALUE_CLASS); + } else if (_this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) { + $inputContainer.removeClass(HAS_VALUE_CLASS); + } + if (keyCode === 13) { + return false; + } + if (_this.options.remote) { + clearTimeout(timeout); + return timeout = setTimeout(function() { + var blur_field; + blur_field = _this.shouldBlur(keyCode); + if (blur_field && _this.filterInputBlur) { + _this.input.blur(); + } + return _this.options.query(_this.input.val(), function(data) { + return _this.options.callback(data); + }); + }, 250); + } else { + return _this.filter(_this.input.val()); + } + }; + })(this)); + } + + GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) { + return BLUR_KEYCODES.indexOf(keyCode) >= 0; + }; + + GitLabDropdownFilter.prototype.filter = function(search_text) { + var data, elements, group, key, results, tmp; + if (this.options.onFilter) { + this.options.onFilter(search_text); + } + data = this.options.data(); + if ((data != null) && !this.options.filterByText) { + results = data; + if (search_text !== '') { + if (_.isArray(data)) { + results = fuzzaldrinPlus.filter(data, search_text, { + key: this.options.keys + }); + } else { + if (gl.utils.isObject(data)) { + results = {}; + for (key in data) { + group = data[key]; + tmp = fuzzaldrinPlus.filter(group, search_text, { + key: this.options.keys + }); + if (tmp.length) { + results[key] = tmp.map(function(item) { + return item; + }); + } + } + } + } + } + return this.options.callback(results); + } else { + elements = this.options.elements(); + if (search_text) { + return elements.each(function() { + var $el, matches; + $el = $(this); + matches = fuzzaldrinPlus.match($el.text().trim(), search_text); + if (!$el.is('.dropdown-header')) { + if (matches.length) { + return $el.show(); + } else { + return $el.hide(); + } + } + }); + } else { + return elements.show(); + } + } + }; + + return GitLabDropdownFilter; + + })(); + + GitLabDropdownRemote = (function() { + function GitLabDropdownRemote(dataEndpoint, options) { + this.dataEndpoint = dataEndpoint; + this.options = options; + } + + GitLabDropdownRemote.prototype.execute = function() { + if (typeof this.dataEndpoint === "string") { + return this.fetchData(); + } else if (typeof this.dataEndpoint === "function") { + if (this.options.beforeSend) { + this.options.beforeSend(); + } + return this.dataEndpoint("", (function(_this) { + return function(data) { + if (_this.options.success) { + _this.options.success(data); + } + if (_this.options.beforeSend) { + return _this.options.beforeSend(); + } + }; + })(this)); + } + }; + + GitLabDropdownRemote.prototype.fetchData = function() { + return $.ajax({ + url: this.dataEndpoint, + dataType: this.options.dataType, + beforeSend: (function(_this) { + return function() { + if (_this.options.beforeSend) { + return _this.options.beforeSend(); + } + }; + })(this), + success: (function(_this) { + return function(data) { + if (_this.options.success) { + return _this.options.success(data); + } + }; + })(this) + }); + }; + + return GitLabDropdownRemote; + + })(); + + GitLabDropdown = (function() { + var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, currentIndex; + + LOADING_CLASS = "is-loading"; + + PAGE_TWO_CLASS = "is-page-two"; + + ACTIVE_CLASS = "is-active"; + + INDETERMINATE_CLASS = "is-indeterminate"; + + currentIndex = -1; + + FILTER_INPUT = '.dropdown-input .dropdown-input-field'; + + function GitLabDropdown(el1, options) { + var ref, ref1, ref2, ref3, searchFields, selector, self; + this.el = el1; + this.options = options; + this.updateLabel = bind(this.updateLabel, this); + this.hidden = bind(this.hidden, this); + this.opened = bind(this.opened, this); + this.shouldPropagate = bind(this.shouldPropagate, this); + self = this; + selector = $(this.el).data("target"); + this.dropdown = selector != null ? $(selector) : $(this.el).parent(); + ref = this.options, this.filterInput = (ref1 = ref.filterInput) != null ? ref1 : this.getElement(FILTER_INPUT), this.highlight = (ref2 = ref.highlight) != null ? ref2 : false, this.filterInputBlur = (ref3 = ref.filterInputBlur) != null ? ref3 : true; + self = this; + if (_.isString(this.filterInput)) { + this.filterInput = this.getElement(this.filterInput); + } + searchFields = this.options.search ? this.options.search.fields : []; + if (this.options.data) { + if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) { + this.fullData = this.options.data; + this.parseData(this.options.data); + } else { + this.remote = new GitLabDropdownRemote(this.options.data, { + dataType: this.options.dataType, + beforeSend: this.toggleLoading.bind(this), + success: (function(_this) { + return function(data) { + _this.fullData = data; + _this.parseData(_this.fullData); + if (_this.options.filterable && _this.filter && _this.filter.input) { + return _this.filter.input.trigger('keyup'); + } + }; + })(this) + }); + } + } + if (this.options.filterable) { + this.filter = new GitLabDropdownFilter(this.filterInput, { + filterInputBlur: this.filterInputBlur, + filterByText: this.options.filterByText, + onFilter: this.options.onFilter, + remote: this.options.filterRemote, + query: this.options.data, + keys: searchFields, + elements: (function(_this) { + return function() { + selector = '.dropdown-content li:not(.divider)'; + if (_this.dropdown.find('.dropdown-toggle-page').length) { + selector = ".dropdown-page-one " + selector; + } + return $(selector); + }; + })(this), + data: (function(_this) { + return function() { + return _this.fullData; + }; + })(this), + callback: (function(_this) { + return function(data) { + _this.parseData(data); + if (_this.filterInput.val() !== '') { + selector = '.dropdown-content li:not(.divider):visible'; + if (_this.dropdown.find('.dropdown-toggle-page').length) { + selector = ".dropdown-page-one " + selector; + } + $(selector, _this.dropdown).first().find('a').addClass('is-focused'); + return currentIndex = 0; + } + }; + })(this) + }); + } + this.dropdown.on("shown.bs.dropdown", this.opened); + this.dropdown.on("hidden.bs.dropdown", this.hidden); + $(this.el).on("update.label", this.updateLabel); + this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate); + this.dropdown.on('keyup', (function(_this) { + return function(e) { + if (e.which === 27) { + return $('.dropdown-menu-close', _this.dropdown).trigger('click'); + } + }; + })(this)); + this.dropdown.on('blur', 'a', (function(_this) { + return function(e) { + var $dropdownMenu, $relatedTarget; + if (e.relatedTarget != null) { + $relatedTarget = $(e.relatedTarget); + $dropdownMenu = $relatedTarget.closest('.dropdown-menu'); + if ($dropdownMenu.length === 0) { + return _this.dropdown.removeClass('open'); + } + } + }; + })(this)); + if (this.dropdown.find(".dropdown-toggle-page").length) { + this.dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on("click", (function(_this) { + return function(e) { + e.preventDefault(); + e.stopPropagation(); + return _this.togglePage(); + }; + })(this)); + } + if (this.options.selectable) { + selector = ".dropdown-content a"; + if (this.dropdown.find(".dropdown-toggle-page").length) { + selector = ".dropdown-page-one .dropdown-content a"; + } + this.dropdown.on("click", selector, function(e) { + var $el, selected; + $el = $(this); + selected = self.rowClicked($el); + if (self.options.clicked) { + self.options.clicked(selected, $el, e); + } + return $el.trigger('blur'); + }); + } + } + + GitLabDropdown.prototype.getElement = function(selector) { + return this.dropdown.find(selector); + }; + + GitLabDropdown.prototype.toggleLoading = function() { + return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS); + }; + + GitLabDropdown.prototype.togglePage = function() { + var menu; + menu = $('.dropdown-menu', this.dropdown); + if (menu.hasClass(PAGE_TWO_CLASS)) { + if (this.remote) { + this.remote.execute(); + } + } + menu.toggleClass(PAGE_TWO_CLASS); + return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus(); + }; + + GitLabDropdown.prototype.parseData = function(data) { + var full_html, groupData, html, name; + this.renderedData = data; + if (this.options.filterable && data.length === 0) { + html = [this.noResults()]; + } else { + if (gl.utils.isObject(data)) { + html = []; + for (name in data) { + groupData = data[name]; + html.push(this.renderItem({ + header: name + }, name)); + this.renderData(groupData, name).map(function(item) { + return html.push(item); + }); + } + } else { + html = this.renderData(data); + } + } + full_html = this.renderMenu(html); + return this.appendMenu(full_html); + }; + + GitLabDropdown.prototype.renderData = function(data, group) { + if (group == null) { + group = false; + } + return data.map((function(_this) { + return function(obj, index) { + return _this.renderItem(obj, group, index); + }; + })(this)); + }; + + GitLabDropdown.prototype.shouldPropagate = function(e) { + var $target; + if (this.options.multiSelect) { + $target = $(e.target); + if (!$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) { + e.stopPropagation(); + return false; + } else { + return true; + } + } + }; + + GitLabDropdown.prototype.opened = function() { + var contentHtml; + this.addArrowKeyEvent(); + if (this.options.setIndeterminateIds) { + this.options.setIndeterminateIds.call(this); + } + if (this.options.setActiveIds) { + this.options.setActiveIds.call(this); + } + if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { + this.parseData(this.fullData); + } + contentHtml = $('.dropdown-content', this.dropdown).html(); + if (this.remote && contentHtml === "") { + this.remote.execute(); + } + if (this.options.filterable) { + this.filterInput.focus(); + } + return this.dropdown.trigger('shown.gl.dropdown'); + }; + + GitLabDropdown.prototype.hidden = function(e) { + var $input; + this.removeArrayKeyEvent(); + $input = this.dropdown.find(".dropdown-input-field"); + if (this.options.filterable) { + $input.blur().val(""); + } + if (!this.options.persistWhenHide) { + $input.trigger("keyup"); + } + if (this.dropdown.find(".dropdown-toggle-page").length) { + $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); + } + if (this.options.hidden) { + this.options.hidden.call(this, e); + } + return this.dropdown.trigger('hidden.gl.dropdown'); + }; + + GitLabDropdown.prototype.renderMenu = function(html) { + var menu_html; + menu_html = ""; + if (this.options.renderMenu) { + menu_html = this.options.renderMenu(html); + } else { + menu_html = $('<ul />').append(html); + } + return menu_html; + }; + + GitLabDropdown.prototype.appendMenu = function(html) { + var selector; + selector = '.dropdown-content'; + if (this.dropdown.find(".dropdown-toggle-page").length) { + selector = ".dropdown-page-one .dropdown-content"; + } + return $(selector, this.dropdown).empty().append(html); + }; + + GitLabDropdown.prototype.renderItem = function(data, group, index) { + var cssClass, field, fieldName, groupAttrs, html, selected, text, url, value; + if (group == null) { + group = false; + } + if (index == null) { + index = false; + } + html = ""; + if (data === "divider") { + return "<li class='divider'></li>"; + } + if (data === "separator") { + return "<li class='separator'></li>"; + } + if (data.header != null) { + return "<li class='dropdown-header'>" + data.header + "</li>"; + } + if (this.options.renderRow) { + html = this.options.renderRow.call(this.options, data, this); + } else { + if (!selected) { + value = this.options.id ? this.options.id(data) : data.id; + fieldName = this.options.fieldName; + field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); + if (field.length) { + selected = true; + } + } + if (this.options.url != null) { + url = this.options.url(data); + } else { + url = data.url != null ? data.url : '#'; + } + if (this.options.text != null) { + text = this.options.text(data); + } else { + text = data.text != null ? data.text : ''; + } + cssClass = ""; + if (selected) { + cssClass = "is-active"; + } + if (this.highlight) { + text = this.highlightTextMatches(text, this.filterInput.val()); + } + if (group) { + groupAttrs = "data-group='" + group + "' data-index='" + index + "'"; + } else { + groupAttrs = ''; + } + html = "<li> <a href='" + url + "' " + groupAttrs + " class='" + cssClass + "'> " + text + " </a> </li>"; + } + return html; + }; + + GitLabDropdown.prototype.highlightTextMatches = function(text, term) { + var occurrences; + occurrences = fuzzaldrinPlus.match(text, term); + return text.split('').map(function(character, i) { + if (indexOf.call(occurrences, i) >= 0) { + return "<b>" + character + "</b>"; + } else { + return character; + } + }).join(''); + }; + + GitLabDropdown.prototype.noResults = function() { + var html; + return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>"; + }; + + GitLabDropdown.prototype.highlightRow = function(index) { + var selector; + if (this.filterInput.val() !== "") { + selector = '.dropdown-content li:first-child a'; + if (this.dropdown.find(".dropdown-toggle-page").length) { + selector = ".dropdown-page-one .dropdown-content li:first-child a"; + } + return this.getElement(selector).addClass('is-focused'); + } + }; + + GitLabDropdown.prototype.rowClicked = function(el) { + var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value; + fieldName = this.options.fieldName; + isInput = $(this.el).is('input'); + if (this.renderedData) { + groupName = el.data('group'); + if (groupName) { + selectedIndex = el.data('index'); + selectedObject = this.renderedData[groupName][selectedIndex]; + } else { + selectedIndex = el.closest('li').index(); + selectedObject = this.renderedData[selectedIndex]; + } + } + value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id; + if (isInput) { + field = $(this.el); + } else { + field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); + } + if (el.hasClass(ACTIVE_CLASS)) { + el.removeClass(ACTIVE_CLASS); + if (isInput) { + field.val(''); + } else { + field.remove(); + } + if (this.options.toggleLabel) { + return this.updateLabel(selectedObject, el, this); + } else { + return selectedObject; + } + } else if (el.hasClass(INDETERMINATE_CLASS)) { + el.addClass(ACTIVE_CLASS); + el.removeClass(INDETERMINATE_CLASS); + if (value == null) { + field.remove(); + } + if (!field.length && fieldName) { + this.addInput(fieldName, value); + } + return selectedObject; + } else { + if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) { + this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS); + if (!isInput) { + this.dropdown.parent().find("input[name='" + fieldName + "']").remove(); + } + } + if (value == null) { + field.remove(); + } + el.addClass(ACTIVE_CLASS); + if (this.options.toggleLabel) { + this.updateLabel(selectedObject, el, this); + } + if (value != null) { + if (!field.length && fieldName) { + this.addInput(fieldName, value); + } else { + field.val(value).trigger('change'); + } + } + return selectedObject; + } + }; + + GitLabDropdown.prototype.addInput = function(fieldName, value) { + var $input; + $input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value); + if (this.options.inputId != null) { + $input.attr('id', this.options.inputId); + } + return this.dropdown.before($input); + }; + + GitLabDropdown.prototype.selectRowAtIndex = function(e, index) { + var $el, selector; + selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(" + index + ") a"; + if (this.dropdown.find(".dropdown-toggle-page").length) { + selector = ".dropdown-page-one " + selector; + } + $el = $(selector, this.dropdown); + if ($el.length) { + e.preventDefault(); + e.stopImmediatePropagation(); + return $el.first().trigger('click'); + } + }; + + GitLabDropdown.prototype.addArrowKeyEvent = function() { + var $input, ARROW_KEY_CODES, selector; + ARROW_KEY_CODES = [38, 40]; + $input = this.dropdown.find(".dropdown-input-field"); + selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator)'; + if (this.dropdown.find(".dropdown-toggle-page").length) { + selector = ".dropdown-page-one " + selector; + } + return $('body').on('keydown', (function(_this) { + return function(e) { + var $listItems, PREV_INDEX, currentKeyCode; + currentKeyCode = e.which; + if (ARROW_KEY_CODES.indexOf(currentKeyCode) >= 0) { + e.preventDefault(); + e.stopImmediatePropagation(); + PREV_INDEX = currentIndex; + $listItems = $(selector, _this.dropdown); + if (currentKeyCode === 40) { + if (currentIndex < ($listItems.length - 1)) { + currentIndex += 1; + } + } else if (currentKeyCode === 38) { + if (currentIndex > 0) { + currentIndex -= 1; + } + } + if (currentIndex !== PREV_INDEX) { + _this.highlightRowAtIndex($listItems, currentIndex); + } + return false; + } + if (currentKeyCode === 13 && currentIndex !== -1) { + return _this.selectRowAtIndex(e, currentIndex); + } + }; + })(this)); + }; + + GitLabDropdown.prototype.removeArrayKeyEvent = function() { + return $('body').off('keydown'); + }; + + GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) { + var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop; + $('.is-focused', this.dropdown).removeClass('is-focused'); + $listItem = $listItems.eq(index); + $listItem.find('a:first-child').addClass("is-focused"); + $dropdownContent = $listItem.closest('.dropdown-content'); + dropdownScrollTop = $dropdownContent.scrollTop(); + dropdownContentHeight = $dropdownContent.outerHeight(); + dropdownContentTop = $dropdownContent.prop('offsetTop'); + dropdownContentBottom = dropdownContentTop + dropdownContentHeight; + listItemHeight = $listItem.outerHeight(); + listItemTop = $listItem.prop('offsetTop'); + listItemBottom = listItemTop + listItemHeight; + if (listItemBottom > dropdownContentBottom + dropdownScrollTop) { + return $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom); + } else if (listItemTop < dropdownContentTop + dropdownScrollTop) { + return $dropdownContent.scrollTop(listItemTop - dropdownContentTop); + } + }; + + GitLabDropdown.prototype.updateLabel = function(selected, el, instance) { + if (selected == null) { + selected = null; + } + if (el == null) { + el = null; + } + if (instance == null) { + instance = null; + } + return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance)); + }; + + return GitLabDropdown; + + })(); + + $.fn.glDropdown = function(opts) { + return this.each(function() { + if (!$.data(this, 'glDropdown')) { + return $.data(this, 'glDropdown', new GitLabDropdown(this, opts)); + } + }); + }; + +}).call(this); diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee deleted file mode 100644 index 951530e03a5..00000000000 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ /dev/null @@ -1,625 +0,0 @@ -class GitLabDropdownFilter - BLUR_KEYCODES = [27, 40] - ARROW_KEY_CODES = [38, 40] - HAS_VALUE_CLASS = "has-value" - - constructor: (@input, @options) -> - { - @filterInputBlur = true - } = @options - - $inputContainer = @input.parent() - $clearButton = $inputContainer.find('.js-dropdown-input-clear') - - @indeterminateIds = [] - - # Clear click - $clearButton.on 'click', (e) => - e.preventDefault() - e.stopPropagation() - @input - .val('') - .trigger('keyup') - .focus() - - # Key events - timeout = "" - @input.on "keyup", (e) => - keyCode = e.which - - return if ARROW_KEY_CODES.indexOf(keyCode) >= 0 - - if @input.val() isnt "" and !$inputContainer.hasClass HAS_VALUE_CLASS - $inputContainer.addClass HAS_VALUE_CLASS - else if @input.val() is "" and $inputContainer.hasClass HAS_VALUE_CLASS - $inputContainer.removeClass HAS_VALUE_CLASS - - if keyCode is 13 - return false - - # Only filter asynchronously only if option remote is set - if @options.remote - clearTimeout timeout - timeout = setTimeout => - blur_field = @shouldBlur keyCode - - if blur_field and @filterInputBlur - @input.blur() - - @options.query @input.val(), (data) => - @options.callback(data) - , 250 - else - @filter @input.val() - - shouldBlur: (keyCode) -> - return BLUR_KEYCODES.indexOf(keyCode) >= 0 - - filter: (search_text) -> - @options.onFilter(search_text) if @options.onFilter - data = @options.data() - - if data? and not @options.filterByText - results = data - - if search_text isnt '' - # When data is an array of objects therefore [object Array] e.g. - # [ - # { prop: 'foo' }, - # { prop: 'baz' } - # ] - if _.isArray(data) - results = fuzzaldrinPlus.filter(data, search_text, - key: @options.keys - ) - else - # If data is grouped therefore an [object Object]. e.g. - # { - # groupName1: [ - # { prop: 'foo' }, - # { prop: 'baz' } - # ], - # groupName2: [ - # { prop: 'abc' }, - # { prop: 'def' } - # ] - # } - if gl.utils.isObject data - results = {} - for key, group of data - tmp = fuzzaldrinPlus.filter(group, search_text, - key: @options.keys - ) - - if tmp.length - results[key] = tmp.map (item) -> item - - @options.callback results - else - elements = @options.elements() - - if search_text - elements.each -> - $el = $(@) - matches = fuzzaldrinPlus.match($el.text().trim(), search_text) - - unless $el.is('.dropdown-header') - if matches.length - $el.show() - else - $el.hide() - else - elements.show() - -class GitLabDropdownRemote - constructor: (@dataEndpoint, @options) -> - - execute: -> - if typeof @dataEndpoint is "string" - @fetchData() - else if typeof @dataEndpoint is "function" - if @options.beforeSend - @options.beforeSend() - - # Fetch the data by calling the data funcfion - @dataEndpoint "", (data) => - if @options.success - @options.success(data) - - if @options.beforeSend - @options.beforeSend() - - # Fetch the data through ajax if the data is a string - fetchData: -> - $.ajax( - url: @dataEndpoint, - dataType: @options.dataType, - beforeSend: => - if @options.beforeSend - @options.beforeSend() - success: (data) => - if @options.success - @options.success(data) - ) - -class GitLabDropdown - LOADING_CLASS = "is-loading" - PAGE_TWO_CLASS = "is-page-two" - ACTIVE_CLASS = "is-active" - INDETERMINATE_CLASS = "is-indeterminate" - currentIndex = -1 - - FILTER_INPUT = '.dropdown-input .dropdown-input-field' - - constructor: (@el, @options) -> - self = @ - selector = $(@el).data "target" - @dropdown = if selector? then $(selector) else $(@el).parent() - - # Set Defaults - { - # If no input is passed create a default one - @filterInput = @getElement(FILTER_INPUT) - @highlight = false - @filterInputBlur = true - } = @options - - self = @ - - # If selector was passed - if _.isString(@filterInput) - @filterInput = @getElement(@filterInput) - - searchFields = if @options.search then @options.search.fields else []; - - if @options.data - # If we provided data - # data could be an array of objects or a group of arrays - if _.isObject(@options.data) and not _.isFunction(@options.data) - @fullData = @options.data - @parseData @options.data - else - # Remote data - @remote = new GitLabDropdownRemote @options.data, { - dataType: @options.dataType, - beforeSend: @toggleLoading.bind(@) - success: (data) => - @fullData = data - - @parseData @fullData - - @filter.input.trigger('keyup') if @options.filterable and @filter and @filter.input - } - - # Init filterable - if @options.filterable - @filter = new GitLabDropdownFilter @filterInput, - filterInputBlur: @filterInputBlur - filterByText: @options.filterByText - onFilter: @options.onFilter - remote: @options.filterRemote - query: @options.data - keys: searchFields - elements: => - selector = '.dropdown-content li:not(.divider)' - - if @dropdown.find('.dropdown-toggle-page').length - selector = ".dropdown-page-one #{selector}" - - return $(selector) - data: => - return @fullData - callback: (data) => - currentIndex = -1 - @parseData data - - # Event listeners - - @dropdown.on "shown.bs.dropdown", @opened - @dropdown.on "hidden.bs.dropdown", @hidden - $(@el).on "update.label", @updateLabel - @dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate - @dropdown.on 'keyup', (e) => - if e.which is 27 # Escape key - $('.dropdown-menu-close', @dropdown).trigger 'click' - @dropdown.on 'blur', 'a', (e) => - if e.relatedTarget? - $relatedTarget = $(e.relatedTarget) - $dropdownMenu = $relatedTarget.closest('.dropdown-menu') - - if $dropdownMenu.length is 0 - @dropdown.removeClass('open') - - if @dropdown.find(".dropdown-toggle-page").length - @dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) => - e.preventDefault() - e.stopPropagation() - - @togglePage() - - if @options.selectable - selector = ".dropdown-content a" - - if @dropdown.find(".dropdown-toggle-page").length - selector = ".dropdown-page-one .dropdown-content a" - - @dropdown.on "click", selector, (e) -> - $el = $(@) - selected = self.rowClicked $el - - if self.options.clicked - self.options.clicked(selected, $el, e) - - $el.trigger('blur') - - # Finds an element inside wrapper element - getElement: (selector) -> - @dropdown.find selector - - toggleLoading: -> - $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS - - togglePage: -> - menu = $('.dropdown-menu', @dropdown) - - if menu.hasClass(PAGE_TWO_CLASS) - if @remote - @remote.execute() - - menu.toggleClass PAGE_TWO_CLASS - - # Focus first visible input on active page - @dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus() - - parseData: (data) -> - @renderedData = data - - if @options.filterable and data.length is 0 - # render no matching results - html = [@noResults()] - else - # Handle array groups - if gl.utils.isObject data - html = [] - for name, groupData of data - # Add header for each group - html.push(@renderItem(header: name, name)) - - @renderData(groupData, name) - .map (item) -> - html.push item - else - # Render each row - html = @renderData(data) - - # Render the full menu - full_html = @renderMenu(html) - - @appendMenu(full_html) - - renderData: (data, group = false) -> - data.map (obj, index) => - return @renderItem(obj, group, index) - - shouldPropagate: (e) => - if @options.multiSelect - $target = $(e.target) - - if not $target.hasClass('dropdown-menu-close') and not $target.hasClass('dropdown-menu-close-icon') and not $target.data('is-link') - e.stopPropagation() - return false - else - return true - - opened: => - @addArrowKeyEvent() - - if @options.setIndeterminateIds - @options.setIndeterminateIds.call(@) - - if @options.setActiveIds - @options.setActiveIds.call(@) - - # Makes indeterminate items effective - if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') - @parseData @fullData - - contentHtml = $('.dropdown-content', @dropdown).html() - if @remote && contentHtml is "" - @remote.execute() - - if @options.filterable - @filterInput.focus() - - @dropdown.trigger('shown.gl.dropdown') - - hidden: (e) => - @removeArrayKeyEvent() - - $input = @dropdown.find(".dropdown-input-field") - - if @options.filterable - $input - .blur() - .val("") - - # Triggering 'keyup' will re-render the dropdown which is not always required - # specially if we want to keep the state of the dropdown needed for bulk-assignment - if not @options.persistWhenHide - $input.trigger("keyup") - - if @dropdown.find(".dropdown-toggle-page").length - $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS - - if @options.hidden - @options.hidden.call(@,e) - - @dropdown.trigger('hidden.gl.dropdown') - - - # Render the full menu - renderMenu: (html) -> - menu_html = "" - - if @options.renderMenu - menu_html = @options.renderMenu(html) - else - menu_html = $('<ul />') - .append(html) - - return menu_html - - # Append the menu into the dropdown - appendMenu: (html) -> - selector = '.dropdown-content' - if @dropdown.find(".dropdown-toggle-page").length - selector = ".dropdown-page-one .dropdown-content" - $(selector, @dropdown) - .empty() - .append(html) - - # Render the row - renderItem: (data, group = false, index = false) -> - html = "" - - # Divider - return "<li class='divider'></li>" if data is "divider" - - # Separator is a full-width divider - return "<li class='separator'></li>" if data is "separator" - - # Header - return "<li class='dropdown-header'>#{data.header}</li>" if data.header? - - if @options.renderRow - # Call the render function - html = @options.renderRow.call(@options, data, @) - else - if not selected - value = if @options.id then @options.id(data) else data.id - fieldName = @options.fieldName - field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") - if field.length - selected = true - - # Set URL - if @options.url? - url = @options.url(data) - else - url = if data.url? then data.url else '#' - - # Set Text - if @options.text? - text = @options.text(data) - else - text = if data.text? then data.text else '' - - cssClass = ""; - - if selected - cssClass = "is-active" - - if @highlight - text = @highlightTextMatches(text, @filterInput.val()) - - if group - groupAttrs = "data-group='#{group}' data-index='#{index}'" - else - groupAttrs = '' - - html = "<li> - <a href='#{url}' #{groupAttrs} class='#{cssClass}'> - #{text} - </a> - </li>" - - return html - - highlightTextMatches: (text, term) -> - occurrences = fuzzaldrinPlus.match(text, term) - text.split('').map((character, i) -> - if i in occurrences then "<b>#{character}</b>" else character - ).join('') - - noResults: -> - html = "<li class='dropdown-menu-empty-link'> - <a href='#' class='is-focused'> - No matching results. - </a> - </li>" - - highlightRow: (index) -> - if @filterInput.val() isnt "" - selector = '.dropdown-content li:first-child a' - if @dropdown.find(".dropdown-toggle-page").length - selector = ".dropdown-page-one .dropdown-content li:first-child a" - - @getElement(selector).addClass 'is-focused' - - rowClicked: (el) -> - fieldName = @options.fieldName - isInput = $(@el).is('input') - - if @renderedData - groupName = el.data('group') - if groupName - selectedIndex = el.data('index') - selectedObject = @renderedData[groupName][selectedIndex] - else - selectedIndex = el.closest('li').index() - selectedObject = @renderedData[selectedIndex] - - value = if @options.id then @options.id(selectedObject, el) else selectedObject.id - - if isInput - field = $(@el) - else - field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") - - if el.hasClass(ACTIVE_CLASS) - el.removeClass(ACTIVE_CLASS) - - if isInput - field.val('') - else - field.remove() - - # Toggle the dropdown label - if @options.toggleLabel - @updateLabel(selectedObject, el, @) - else - selectedObject - else if el.hasClass(INDETERMINATE_CLASS) - el.addClass ACTIVE_CLASS - el.removeClass INDETERMINATE_CLASS - - if not value? - field.remove() - - if not field.length and fieldName - @addInput(fieldName, value) - - return selectedObject - else - if not @options.multiSelect or el.hasClass('dropdown-clear-active') - @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS - - unless isInput - @dropdown.parent().find("input[name='#{fieldName}']").remove() - - if !value? - field.remove() - - # Toggle active class for the tick mark - el.addClass ACTIVE_CLASS - - # Toggle the dropdown label - if @options.toggleLabel - @updateLabel(selectedObject, el, @) - if value? - if !field.length and fieldName - @addInput(fieldName, value) - else - field - .val value - .trigger 'change' - - return selectedObject - - addInput: (fieldName, value)-> - # Create hidden input for form - $input = $('<input>').attr('type', 'hidden') - .attr('name', fieldName) - .val(value) - - if @options.inputId? - $input.attr('id', @options.inputId) - - @dropdown.before $input - - selectRowAtIndex: (e, index) -> - selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(#{index}) a" - - if @dropdown.find(".dropdown-toggle-page").length - selector = ".dropdown-page-one #{selector}" - - # simulate a click on the first link - $el = $(selector, @dropdown) - - if $el.length - e.preventDefault() - e.stopImmediatePropagation() - $el.first().trigger('click') - - addArrowKeyEvent: -> - ARROW_KEY_CODES = [38, 40] - $input = @dropdown.find(".dropdown-input-field") - - selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator)' - if @dropdown.find(".dropdown-toggle-page").length - selector = ".dropdown-page-one #{selector}" - - $('body').on 'keydown', (e) => - currentKeyCode = e.which - - if ARROW_KEY_CODES.indexOf(currentKeyCode) >= 0 - e.preventDefault() - e.stopImmediatePropagation() - - PREV_INDEX = currentIndex - $listItems = $(selector, @dropdown) - - # if @options.filterable - # $input.blur() - - if currentKeyCode is 40 - # Move down - currentIndex += 1 if currentIndex < ($listItems.length - 1) - else if currentKeyCode is 38 - # Move up - currentIndex -= 1 if currentIndex > 0 - - @highlightRowAtIndex($listItems, currentIndex) if currentIndex isnt PREV_INDEX - - return false - - if currentKeyCode is 13 and currentIndex isnt -1 - @selectRowAtIndex e, currentIndex - - removeArrayKeyEvent: -> - $('body').off 'keydown' - - highlightRowAtIndex: ($listItems, index) -> - # Remove the class for the previously focused row - $('.is-focused', @dropdown).removeClass 'is-focused' - - # Update the class for the row at the specific index - $listItem = $listItems.eq(index) - $listItem.find('a:first-child').addClass "is-focused" - - # Dropdown content scroll area - $dropdownContent = $listItem.closest('.dropdown-content') - dropdownScrollTop = $dropdownContent.scrollTop() - dropdownContentHeight = $dropdownContent.outerHeight() - dropdownContentTop = $dropdownContent.prop('offsetTop') - dropdownContentBottom = dropdownContentTop + dropdownContentHeight - - # Get the offset bottom of the list item - listItemHeight = $listItem.outerHeight() - listItemTop = $listItem.prop('offsetTop') - listItemBottom = listItemTop + listItemHeight - - if listItemBottom > dropdownContentBottom + dropdownScrollTop - # Scroll the dropdown content down - $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom) - else if listItemTop < dropdownContentTop + dropdownScrollTop - # Scroll the dropdown content up - $dropdownContent.scrollTop(listItemTop - dropdownContentTop) - - updateLabel: (selected = null, el = null, instance = null) => - $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selected, el, instance) - -$.fn.glDropdown = (opts) -> - return @.each -> - if (!$.data @, 'glDropdown') - $.data(@, 'glDropdown', new GitLabDropdown @, opts) diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js new file mode 100644 index 00000000000..6ac7564a848 --- /dev/null +++ b/app/assets/javascripts/gl_form.js @@ -0,0 +1,53 @@ +(function() { + this.GLForm = (function() { + function GLForm(form) { + this.form = form; + this.textarea = this.form.find('textarea.js-gfm-input'); + this.destroy(); + this.setupForm(); + this.form.data('gl-form', this); + } + + GLForm.prototype.destroy = function() { + this.clearEventListeners(); + return this.form.data('gl-form', null); + }; + + GLForm.prototype.setupForm = function() { + var isNewForm; + isNewForm = this.form.is(':not(.gfm-form)'); + this.form.removeClass('js-new-note-form'); + if (isNewForm) { + this.form.find('.div-dropzone').remove(); + this.form.addClass('gfm-form'); + disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button')); + GitLab.GfmAutoComplete.setup(); + new DropzoneInput(this.form); + autosize(this.textarea); + this.addEventListeners(); + gl.text.init(this.form); + } + this.form.find('.js-note-discard').hide(); + return this.form.show(); + }; + + GLForm.prototype.clearEventListeners = function() { + this.textarea.off('focus'); + this.textarea.off('blur'); + return gl.text.removeListeners(this.form); + }; + + GLForm.prototype.addEventListeners = function() { + this.textarea.on('focus', function() { + return $(this).closest('.md-area').addClass('is-focused'); + }); + return this.textarea.on('blur', function() { + return $(this).closest('.md-area').removeClass('is-focused'); + }); + }; + + return GLForm; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/gl_form.js.coffee b/app/assets/javascripts/gl_form.js.coffee deleted file mode 100644 index 77512d187c9..00000000000 --- a/app/assets/javascripts/gl_form.js.coffee +++ /dev/null @@ -1,54 +0,0 @@ -class @GLForm - constructor: (@form) -> - @textarea = @form.find('textarea.js-gfm-input') - - # Before we start, we should clean up any previous data for this form - @destroy() - - # Setup the form - @setupForm() - - @form.data 'gl-form', @ - - destroy: -> - # Clean form listeners - @clearEventListeners() - @form.data 'gl-form', null - - setupForm: -> - isNewForm = @form.is(':not(.gfm-form)') - - @form.removeClass 'js-new-note-form' - - if isNewForm - @form.find('.div-dropzone').remove() - @form.addClass('gfm-form') - disableButtonIfEmptyField @form.find('.js-note-text'), @form.find('.js-comment-button') - - # remove notify commit author checkbox for non-commit notes - GitLab.GfmAutoComplete.setup() - new DropzoneInput(@form) - - autosize(@textarea) - - # form and textarea event listeners - @addEventListeners() - - gl.text.init(@form) - - # hide discard button - @form.find('.js-note-discard').hide() - - @form.show() - - clearEventListeners: -> - @textarea.off 'focus' - @textarea.off 'blur' - gl.text.removeListeners(@form) - - addEventListeners: -> - @textarea.on 'focus', -> - $(@).closest('.md-area').addClass 'is-focused' - - @textarea.on 'blur', -> - $(@).closest('.md-area').removeClass 'is-focused' diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js new file mode 100644 index 00000000000..b95faadc8e7 --- /dev/null +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -0,0 +1,7 @@ + +/*= require_tree . */ + +(function() { + + +}).call(this); diff --git a/app/assets/javascripts/graphs/graphs_bundle.js.coffee b/app/assets/javascripts/graphs/graphs_bundle.js.coffee deleted file mode 100644 index e0f681acf0b..00000000000 --- a/app/assets/javascripts/graphs/graphs_bundle.js.coffee +++ /dev/null @@ -1,7 +0,0 @@ -# This is a manifest file that'll be compiled into including all the files listed below. -# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically -# be included in the compiled file accessible from http://example.com/assets/application.js -# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -# the compiled file. -# -#= require_tree . diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js new file mode 100644 index 00000000000..f041980bc19 --- /dev/null +++ b/app/assets/javascripts/graphs/stat_graph.js @@ -0,0 +1,19 @@ +(function() { + this.StatGraph = (function() { + function StatGraph() {} + + StatGraph.log = {}; + + StatGraph.get_log = function() { + return this.log; + }; + + StatGraph.set_log = function(data) { + return this.log = data; + }; + + return StatGraph; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/graphs/stat_graph.js.coffee b/app/assets/javascripts/graphs/stat_graph.js.coffee deleted file mode 100644 index f36c71fd25e..00000000000 --- a/app/assets/javascripts/graphs/stat_graph.js.coffee +++ /dev/null @@ -1,6 +0,0 @@ -class @StatGraph - @log: {} - @get_log: -> - @log - @set_log: (data) -> - @log = data diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js new file mode 100644 index 00000000000..927d241b357 --- /dev/null +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -0,0 +1,112 @@ + +/*= require d3 */ + +(function() { + this.ContributorsStatGraph = (function() { + function ContributorsStatGraph() {} + + ContributorsStatGraph.prototype.init = function(log) { + var author_commits, total_commits; + this.parsed_log = ContributorsStatGraphUtil.parse_log(log); + this.set_current_field("commits"); + total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); + author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field); + this.add_master_graph(total_commits); + this.add_authors_graph(author_commits); + return this.change_date_header(); + }; + + ContributorsStatGraph.prototype.add_master_graph = function(total_data) { + this.master_graph = new ContributorsMasterGraph(total_data); + return this.master_graph.draw(); + }; + + ContributorsStatGraph.prototype.add_authors_graph = function(author_data) { + var limited_author_data; + this.authors = []; + limited_author_data = author_data.slice(0, 100); + return _.each(limited_author_data, (function(_this) { + return function(d) { + var author_graph, author_header; + author_header = _this.create_author_header(d); + $(".contributors-list").append(author_header); + _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates); + return author_graph.draw(); + }; + })(this)); + }; + + ContributorsStatGraph.prototype.format_author_commit_info = function(author) { + var commits; + commits = $('<span/>', { + "class": 'graph-author-commits-count' + }); + commits.text(author.commits + " commits"); + return $('<span/>').append(commits); + }; + + ContributorsStatGraph.prototype.create_author_header = function(author) { + var author_commit_info, author_commit_info_span, author_email, author_name, list_item; + list_item = $('<li/>', { + "class": 'person', + style: 'display: block;' + }); + author_name = $('<h4>' + author.author_name + '</h4>'); + author_email = $('<p class="graph-author-email">' + author.author_email + '</p>'); + author_commit_info_span = $('<span/>', { + "class": 'commits' + }); + author_commit_info = this.format_author_commit_info(author); + author_commit_info_span.html(author_commit_info); + list_item.append(author_name); + list_item.append(author_email); + list_item.append(author_commit_info_span); + return list_item; + }; + + ContributorsStatGraph.prototype.redraw_master = function() { + var total_data; + total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); + this.master_graph.set_data(total_data); + return this.master_graph.redraw(); + }; + + ContributorsStatGraph.prototype.redraw_authors = function() { + var author_commits, x_domain; + $("ol").html(""); + x_domain = ContributorsGraph.prototype.x_domain; + author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain); + return _.each(author_commits, (function(_this) { + return function(d) { + _this.redraw_author_commit_info(d); + $(_this.authors[d.author_name].list_item).appendTo("ol"); + _this.authors[d.author_name].set_data(d.dates); + return _this.authors[d.author_name].redraw(); + }; + })(this)); + }; + + ContributorsStatGraph.prototype.set_current_field = function(field) { + return this.field = field; + }; + + ContributorsStatGraph.prototype.change_date_header = function() { + var print, print_date_format, x_domain; + x_domain = ContributorsGraph.prototype.x_domain; + print_date_format = d3.time.format("%B %e %Y"); + print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]); + return $("#date_header").text(print); + }; + + ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) { + var author_commit_info, author_list_item; + author_list_item = $(this.authors[author.author_name].list_item); + author_commit_info = this.format_author_commit_info(author); + return author_list_item.find("span").html(author_commit_info); + }; + + return ContributorsStatGraph; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee deleted file mode 100644 index 1d9fae7cf79..00000000000 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee +++ /dev/null @@ -1,71 +0,0 @@ -#= require d3 - -class @ContributorsStatGraph - init: (log) -> - @parsed_log = ContributorsStatGraphUtil.parse_log(log) - @set_current_field("commits") - total_commits = ContributorsStatGraphUtil.get_total_data(@parsed_log, @field) - author_commits = ContributorsStatGraphUtil.get_author_data(@parsed_log, @field) - @add_master_graph(total_commits) - @add_authors_graph(author_commits) - @change_date_header() - add_master_graph: (total_data) -> - @master_graph = new ContributorsMasterGraph(total_data) - @master_graph.draw() - add_authors_graph: (author_data) -> - @authors = [] - limited_author_data = author_data.slice(0, 100) - _.each(limited_author_data, (d) => - author_header = @create_author_header(d) - $(".contributors-list").append(author_header) - @authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates) - author_graph.draw() - ) - format_author_commit_info: (author) -> - commits = $('<span/>', { - class: 'graph-author-commits-count' - }) - commits.text(author.commits + " commits") - $('<span/>').append(commits) - - create_author_header: (author) -> - list_item = $('<li/>', { - class: 'person' - style: 'display: block;' - }) - author_name = $('<h4>' + author.author_name + '</h4>') - author_email = $('<p class="graph-author-email">' + author.author_email + '</p>') - author_commit_info_span = $('<span/>', { - class: 'commits' - }) - author_commit_info = @format_author_commit_info(author) - author_commit_info_span.html(author_commit_info) - list_item.append(author_name) - list_item.append(author_email) - list_item.append(author_commit_info_span) - list_item - redraw_master: -> - total_data = ContributorsStatGraphUtil.get_total_data(@parsed_log, @field) - @master_graph.set_data(total_data) - @master_graph.redraw() - redraw_authors: -> - $("ol").html("") - x_domain = ContributorsGraph.prototype.x_domain - author_commits = ContributorsStatGraphUtil.get_author_data(@parsed_log, @field, x_domain) - _.each(author_commits, (d) => - @redraw_author_commit_info(d) - $(@authors[d.author_name].list_item).appendTo("ol") - @authors[d.author_name].set_data(d.dates) - @authors[d.author_name].redraw() - ) - set_current_field: (field) -> - @field = field - change_date_header: -> - x_domain = ContributorsGraph.prototype.x_domain - print_date_format = d3.time.format("%B %e %Y") - print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]) - $("#date_header").text(print) - redraw_author_commit_info: (author) -> - author_list_item = $(@authors[author.author_name].list_item) - author_commit_info = @format_author_commit_info(author) - author_list_item.find("span").html(author_commit_info) diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js new file mode 100644 index 00000000000..a646ca1d84f --- /dev/null +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -0,0 +1,279 @@ + +/*= require d3 */ + +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.ContributorsGraph = (function() { + function ContributorsGraph() {} + + ContributorsGraph.prototype.MARGIN = { + top: 20, + right: 20, + bottom: 30, + left: 50 + }; + + ContributorsGraph.prototype.x_domain = null; + + ContributorsGraph.prototype.y_domain = null; + + ContributorsGraph.prototype.dates = []; + + ContributorsGraph.set_x_domain = function(data) { + return ContributorsGraph.prototype.x_domain = data; + }; + + ContributorsGraph.set_y_domain = function(data) { + return ContributorsGraph.prototype.y_domain = [ + 0, d3.max(data, function(d) { + var ref, ref1; + return d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions; + }) + ]; + }; + + ContributorsGraph.init_x_domain = function(data) { + return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) { + return d.date; + }); + }; + + ContributorsGraph.init_y_domain = function(data) { + return ContributorsGraph.prototype.y_domain = [ + 0, d3.max(data, function(d) { + var ref, ref1; + return d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions; + }) + ]; + }; + + ContributorsGraph.init_domain = function(data) { + ContributorsGraph.init_x_domain(data); + return ContributorsGraph.init_y_domain(data); + }; + + ContributorsGraph.set_dates = function(data) { + return ContributorsGraph.prototype.dates = data; + }; + + ContributorsGraph.prototype.set_x_domain = function() { + return this.x.domain(this.x_domain); + }; + + ContributorsGraph.prototype.set_y_domain = function() { + return this.y.domain(this.y_domain); + }; + + ContributorsGraph.prototype.set_domain = function() { + this.set_x_domain(); + return this.set_y_domain(); + }; + + ContributorsGraph.prototype.create_scale = function(width, height) { + this.x = d3.time.scale().range([0, width]).clamp(true); + return this.y = d3.scale.linear().range([height, 0]).nice(); + }; + + ContributorsGraph.prototype.draw_x_axis = function() { + return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis); + }; + + ContributorsGraph.prototype.draw_y_axis = function() { + return this.svg.append("g").attr("class", "y axis").call(this.y_axis); + }; + + ContributorsGraph.prototype.set_data = function(data) { + return this.data = data; + }; + + return ContributorsGraph; + + })(); + + this.ContributorsMasterGraph = (function(superClass) { + extend(ContributorsMasterGraph, superClass); + + function ContributorsMasterGraph(data1) { + this.data = data1; + this.update_content = bind(this.update_content, this); + this.width = $('.content').width() - 70; + this.height = 200; + this.x = null; + this.y = null; + this.x_axis = null; + this.y_axis = null; + this.area = null; + this.svg = null; + this.brush = null; + this.x_max_domain = null; + } + + ContributorsMasterGraph.prototype.process_dates = function(data) { + var dates; + dates = this.get_dates(data); + this.parse_dates(data); + return ContributorsGraph.set_dates(dates); + }; + + ContributorsMasterGraph.prototype.get_dates = function(data) { + return _.pluck(data, 'date'); + }; + + ContributorsMasterGraph.prototype.parse_dates = function(data) { + var parseDate; + parseDate = d3.time.format("%Y-%m-%d").parse; + return data.forEach(function(d) { + return d.date = parseDate(d.date); + }); + }; + + ContributorsMasterGraph.prototype.create_scale = function() { + return ContributorsMasterGraph.__super__.create_scale.call(this, this.width, this.height); + }; + + ContributorsMasterGraph.prototype.create_axes = function() { + this.x_axis = d3.svg.axis().scale(this.x).orient("bottom"); + return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); + }; + + ContributorsMasterGraph.prototype.create_svg = function() { + return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + }; + + ContributorsMasterGraph.prototype.create_area = function(x, y) { + return this.area = d3.svg.area().x(function(d) { + return x(d.date); + }).y0(this.height).y1(function(d) { + var ref, ref1, xa; + xa = d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions; + return y(xa); + }).interpolate("basis"); + }; + + ContributorsMasterGraph.prototype.create_brush = function() { + return this.brush = d3.svg.brush().x(this.x).on("brushend", this.update_content); + }; + + ContributorsMasterGraph.prototype.draw_path = function(data) { + return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area); + }; + + ContributorsMasterGraph.prototype.add_brush = function() { + return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height); + }; + + ContributorsMasterGraph.prototype.update_content = function() { + ContributorsGraph.set_x_domain(this.brush.empty() ? this.x_max_domain : this.brush.extent()); + return $("#brush_change").trigger('change'); + }; + + ContributorsMasterGraph.prototype.draw = function() { + this.process_dates(this.data); + this.create_scale(); + this.create_axes(); + ContributorsGraph.init_domain(this.data); + this.x_max_domain = this.x_domain; + this.set_domain(); + this.create_area(this.x, this.y); + this.create_svg(); + this.create_brush(); + this.draw_path(this.data); + this.draw_x_axis(); + this.draw_y_axis(); + return this.add_brush(); + }; + + ContributorsMasterGraph.prototype.redraw = function() { + this.process_dates(this.data); + ContributorsGraph.set_y_domain(this.data); + this.set_y_domain(); + this.svg.select("path").datum(this.data); + this.svg.select("path").attr("d", this.area); + return this.svg.select(".y.axis").call(this.y_axis); + }; + + return ContributorsMasterGraph; + + })(ContributorsGraph); + + this.ContributorsAuthorGraph = (function(superClass) { + extend(ContributorsAuthorGraph, superClass); + + function ContributorsAuthorGraph(data1) { + this.data = data1; + if ($(window).width() < 768) { + this.width = $('.content').width() - 80; + } else { + this.width = ($('.content').width() / 2) - 100; + } + this.height = 200; + this.x = null; + this.y = null; + this.x_axis = null; + this.y_axis = null; + this.area = null; + this.svg = null; + this.list_item = null; + } + + ContributorsAuthorGraph.prototype.create_scale = function() { + return ContributorsAuthorGraph.__super__.create_scale.call(this, this.width, this.height); + }; + + ContributorsAuthorGraph.prototype.create_axes = function() { + this.x_axis = d3.svg.axis().scale(this.x).orient("bottom").ticks(8); + return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); + }; + + ContributorsAuthorGraph.prototype.create_area = function(x, y) { + return this.area = d3.svg.area().x(function(d) { + var parseDate; + parseDate = d3.time.format("%Y-%m-%d").parse; + return x(parseDate(d)); + }).y0(this.height).y1((function(_this) { + return function(d) { + if (_this.data[d] != null) { + return y(_this.data[d]); + } else { + return y(0); + } + }; + })(this)).interpolate("basis"); + }; + + ContributorsAuthorGraph.prototype.create_svg = function() { + this.list_item = d3.selectAll(".person")[0].pop(); + return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + }; + + ContributorsAuthorGraph.prototype.draw_path = function(data) { + return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area); + }; + + ContributorsAuthorGraph.prototype.draw = function() { + this.create_scale(); + this.create_axes(); + this.set_domain(); + this.create_area(this.x, this.y); + this.create_svg(); + this.draw_path(this.dates); + this.draw_x_axis(); + return this.draw_y_axis(); + }; + + ContributorsAuthorGraph.prototype.redraw = function() { + this.set_domain(); + this.svg.select("path").datum(this.dates); + this.svg.select("path").attr("d", this.area); + this.svg.select(".x.axis").call(this.x_axis); + return this.svg.select(".y.axis").call(this.y_axis); + }; + + return ContributorsAuthorGraph; + + })(ContributorsGraph); + +}).call(this); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee deleted file mode 100644 index 834a81af459..00000000000 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee +++ /dev/null @@ -1,173 +0,0 @@ -#= require d3 - -class @ContributorsGraph - MARGIN: - top: 20 - right: 20 - bottom: 30 - left: 50 - x_domain: null - y_domain: null - dates: [] - @set_x_domain: (data) => - @prototype.x_domain = data - @set_y_domain: (data) => - @prototype.y_domain = [0, d3.max(data, (d) -> - d.commits = d.commits ? d.additions ? d.deletions - )] - @init_x_domain: (data) => - @prototype.x_domain = d3.extent(data, (d) -> - d.date - ) - @init_y_domain: (data) => - @prototype.y_domain = [0, d3.max(data, (d) -> - d.commits = d.commits ? d.additions ? d.deletions - )] - @init_domain: (data) => - @init_x_domain(data) - @init_y_domain(data) - @set_dates: (data) => - @prototype.dates = data - set_x_domain: -> - @x.domain(@x_domain) - set_y_domain: -> - @y.domain(@y_domain) - set_domain: -> - @set_x_domain() - @set_y_domain() - create_scale: (width, height) -> - @x = d3.time.scale().range([0, width]).clamp(true) - @y = d3.scale.linear().range([height, 0]).nice() - draw_x_axis: -> - @svg.append("g").attr("class", "x axis").attr("transform", "translate(0, #{@height})") - .call(@x_axis) - draw_y_axis: -> - @svg.append("g").attr("class", "y axis").call(@y_axis) - set_data: (data) -> - @data = data - -class @ContributorsMasterGraph extends ContributorsGraph - constructor: (@data) -> - @width = $('.content').width() - 70 - @height = 200 - @x = null - @y = null - @x_axis = null - @y_axis = null - @area = null - @svg = null - @brush = null - @x_max_domain = null - process_dates: (data) -> - dates = @get_dates(data) - @parse_dates(data) - ContributorsGraph.set_dates(dates) - get_dates: (data) -> - _.pluck(data, 'date') - parse_dates: (data) -> - parseDate = d3.time.format("%Y-%m-%d").parse - data.forEach((d) -> - d.date = parseDate(d.date) - ) - create_scale: -> - super @width, @height - create_axes: -> - @x_axis = d3.svg.axis().scale(@x).orient("bottom") - @y_axis = d3.svg.axis().scale(@y).orient("left").ticks(5) - create_svg: -> - @svg = d3.select("#contributors-master").append("svg") - .attr("width", @width + @MARGIN.left + @MARGIN.right) - .attr("height", @height + @MARGIN.top + @MARGIN.bottom) - .attr("class", "tint-box") - .append("g") - .attr("transform", "translate(" + @MARGIN.left + "," + @MARGIN.top + ")") - create_area: (x, y) -> - @area = d3.svg.area().x((d) -> - x(d.date) - ).y0(@height).y1((d) -> - xa = d.commits = d.commits ? d.additions ? d.deletions - y(xa) - ).interpolate("basis") - create_brush: -> - @brush = d3.svg.brush().x(@x).on("brushend", @update_content) - draw_path: (data) -> - @svg.append("path").datum(data).attr("class", "area").attr("d", @area) - add_brush: -> - @svg.append("g").attr("class", "selection").call(@brush).selectAll("rect").attr("height", @height) - update_content: => - ContributorsGraph.set_x_domain(if @brush.empty() then @x_max_domain else @brush.extent()) - $("#brush_change").trigger('change') - draw: -> - @process_dates(@data) - @create_scale() - @create_axes() - ContributorsGraph.init_domain(@data) - @x_max_domain = @x_domain - @set_domain() - @create_area(@x, @y) - @create_svg() - @create_brush() - @draw_path(@data) - @draw_x_axis() - @draw_y_axis() - @add_brush() - redraw: -> - @process_dates(@data) - ContributorsGraph.set_y_domain(@data) - @set_y_domain() - @svg.select("path").datum(@data) - @svg.select("path").attr("d", @area) - @svg.select(".y.axis").call(@y_axis) - -class @ContributorsAuthorGraph extends ContributorsGraph - constructor: (@data) -> - # Don't split graph size in half for mobile devices. - if $(window).width() < 768 - @width = $('.content').width() - 80 - else - @width = ($('.content').width() / 2) - 100 - @height = 200 - @x = null - @y = null - @x_axis = null - @y_axis = null - @area = null - @svg = null - @list_item = null - create_scale: -> - super @width, @height - create_axes: -> - @x_axis = d3.svg.axis().scale(@x).orient("bottom").ticks(8) - @y_axis = d3.svg.axis().scale(@y).orient("left").ticks(5) - create_area: (x, y) -> - @area = d3.svg.area().x((d) -> - parseDate = d3.time.format("%Y-%m-%d").parse - x(parseDate(d)) - ).y0(@height).y1((d) => - if @data[d]? then y(@data[d]) else y(0) - ).interpolate("basis") - create_svg: -> - @list_item = d3.selectAll(".person")[0].pop() - @svg = d3.select(@list_item).append("svg") - .attr("width", @width + @MARGIN.left + @MARGIN.right) - .attr("height", @height + @MARGIN.top + @MARGIN.bottom) - .attr("class", "spark") - .append("g") - .attr("transform", "translate(" + @MARGIN.left + "," + @MARGIN.top + ")") - draw_path: (data) -> - @svg.append("path").datum(data).attr("class", "area-contributor").attr("d", @area) - draw: -> - @create_scale() - @create_axes() - @set_domain() - @create_area(@x, @y) - @create_svg() - @draw_path(@dates) - @draw_x_axis() - @draw_y_axis() - redraw: -> - @set_domain() - @svg.select("path").datum(@dates) - @svg.select("path").attr("d", @area) - @svg.select(".x.axis").call(@x_axis) - @svg.select(".y.axis").call(@y_axis) diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js new file mode 100644 index 00000000000..0d240bed8b6 --- /dev/null +++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js @@ -0,0 +1,135 @@ +(function() { + window.ContributorsStatGraphUtil = { + parse_log: function(log) { + var by_author, by_email, data, entry, i, len, total; + total = {}; + by_author = {}; + by_email = {}; + for (i = 0, len = log.length; i < len; i++) { + entry = log[i]; + if (total[entry.date] == null) { + this.add_date(entry.date, total); + } + data = by_author[entry.author_name] || by_email[entry.author_email]; + if (data == null) { + data = this.add_author(entry, by_author, by_email); + } + if (!data[entry.date]) { + this.add_date(entry.date, data); + } + this.store_data(entry, total[entry.date], data[entry.date]); + } + total = _.toArray(total); + by_author = _.toArray(by_author); + return { + total: total, + by_author: by_author + }; + }, + add_date: function(date, collection) { + collection[date] = {}; + return collection[date].date = date; + }, + add_author: function(author, by_author, by_email) { + var data; + data = {}; + data.author_name = author.author_name; + data.author_email = author.author_email; + by_author[author.author_name] = data; + return by_email[author.author_email] = data; + }, + store_data: function(entry, total, by_author) { + this.store_commits(total, by_author); + this.store_additions(entry, total, by_author); + return this.store_deletions(entry, total, by_author); + }, + store_commits: function(total, by_author) { + this.add(total, "commits", 1); + return this.add(by_author, "commits", 1); + }, + add: function(collection, field, value) { + if (collection[field] == null) { + collection[field] = 0; + } + return collection[field] += value; + }, + store_additions: function(entry, total, by_author) { + if (entry.additions == null) { + entry.additions = 0; + } + this.add(total, "additions", entry.additions); + return this.add(by_author, "additions", entry.additions); + }, + store_deletions: function(entry, total, by_author) { + if (entry.deletions == null) { + entry.deletions = 0; + } + this.add(total, "deletions", entry.deletions); + return this.add(by_author, "deletions", entry.deletions); + }, + get_total_data: function(parsed_log, field) { + var log, total_data; + log = parsed_log.total; + total_data = this.pick_field(log, field); + return _.sortBy(total_data, function(d) { + return d.date; + }); + }, + pick_field: function(log, field) { + var total_data; + total_data = []; + _.each(log, function(d) { + return total_data.push(_.pick(d, [field, 'date'])); + }); + return total_data; + }, + get_author_data: function(parsed_log, field, date_range) { + var author_data, log; + if (date_range == null) { + date_range = null; + } + log = parsed_log.by_author; + author_data = []; + _.each(log, (function(_this) { + return function(log_entry) { + var parsed_log_entry; + parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range); + if (!_.isEmpty(parsed_log_entry.dates)) { + return author_data.push(parsed_log_entry); + } + }; + })(this)); + return _.sortBy(author_data, function(d) { + return d[field]; + }).reverse(); + }, + parse_log_entry: function(log_entry, field, date_range) { + var parsed_entry; + parsed_entry = {}; + parsed_entry.author_name = log_entry.author_name; + parsed_entry.author_email = log_entry.author_email; + parsed_entry.dates = {}; + parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0; + _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) { + return function(value, key) { + if (_this.in_range(value.date, date_range)) { + parsed_entry.dates[value.date] = value[field]; + parsed_entry.commits += value.commits; + parsed_entry.additions += value.additions; + return parsed_entry.deletions += value.deletions; + } + }; + })(this)); + return parsed_entry; + }, + in_range: function(date, date_range) { + var ref; + if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) { + return true; + } else { + return false; + } + } + }; + +}).call(this); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee deleted file mode 100644 index 31617c88b4a..00000000000 --- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee +++ /dev/null @@ -1,98 +0,0 @@ -window.ContributorsStatGraphUtil = - parse_log: (log) -> - total = {} - by_author = {} - by_email = {} - for entry in log - @add_date(entry.date, total) unless total[entry.date]? - - data = by_author[entry.author_name] || by_email[entry.author_email] - data ?= @add_author(entry, by_author, by_email) - - @add_date(entry.date, data) unless data[entry.date] - @store_data(entry, total[entry.date], data[entry.date]) - total = _.toArray(total) - by_author = _.toArray(by_author) - total: total, by_author: by_author - - add_date: (date, collection) -> - collection[date] = {} - collection[date].date = date - - add_author: (author, by_author, by_email) -> - data = {} - data.author_name = author.author_name - data.author_email = author.author_email - by_author[author.author_name] = data - by_email[author.author_email] = data - - store_data: (entry, total, by_author) -> - @store_commits(total, by_author) - @store_additions(entry, total, by_author) - @store_deletions(entry, total, by_author) - - store_commits: (total, by_author) -> - @add(total, "commits", 1) - @add(by_author, "commits", 1) - - add: (collection, field, value) -> - collection[field] ?= 0 - collection[field] += value - - store_additions: (entry, total, by_author) -> - entry.additions ?= 0 - @add(total, "additions", entry.additions) - @add(by_author, "additions", entry.additions) - - store_deletions: (entry, total, by_author) -> - entry.deletions ?= 0 - @add(total, "deletions", entry.deletions) - @add(by_author, "deletions", entry.deletions) - - get_total_data: (parsed_log, field) -> - log = parsed_log.total - total_data = @pick_field(log, field) - _.sortBy(total_data, (d) -> - d.date - ) - pick_field: (log, field) -> - total_data = [] - _.each(log, (d) -> - total_data.push(_.pick(d, [field, 'date'])) - ) - total_data - - get_author_data: (parsed_log, field, date_range = null) -> - log = parsed_log.by_author - author_data = [] - - _.each(log, (log_entry) => - parsed_log_entry = @parse_log_entry(log_entry, field, date_range) - if not _.isEmpty(parsed_log_entry.dates) - author_data.push(parsed_log_entry) - ) - - _.sortBy(author_data, (d) -> - d[field] - ).reverse() - - parse_log_entry: (log_entry, field, date_range) -> - parsed_entry = {} - parsed_entry.author_name = log_entry.author_name - parsed_entry.author_email = log_entry.author_email - parsed_entry.dates = {} - parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0 - _.each(_.omit(log_entry, 'author_name', 'author_email'), (value, key) => - if @in_range(value.date, date_range) - parsed_entry.dates[value.date] = value[field] - parsed_entry.commits += value.commits - parsed_entry.additions += value.additions - parsed_entry.deletions += value.deletions - ) - return parsed_entry - - in_range: (date, date_range) -> - if date_range is null || date_range[0] <= new Date(date) <= date_range[1] - true - else - false diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js new file mode 100644 index 00000000000..c28ce86d7af --- /dev/null +++ b/app/assets/javascripts/group_avatar.js @@ -0,0 +1,21 @@ +(function() { + this.GroupAvatar = (function() { + function GroupAvatar() { + $('.js-choose-group-avatar-button').bind("click", function() { + var form; + form = $(this).closest("form"); + return form.find(".js-group-avatar-input").click(); + }); + $('.js-group-avatar-input').bind("change", function() { + var filename, form; + form = $(this).closest("form"); + filename = $(this).val().replace(/^.*[\\\/]/, ''); + return form.find(".js-avatar-filename").text(filename); + }); + } + + return GroupAvatar; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/group_avatar.js.coffee b/app/assets/javascripts/group_avatar.js.coffee deleted file mode 100644 index 0825fd3ce52..00000000000 --- a/app/assets/javascripts/group_avatar.js.coffee +++ /dev/null @@ -1,9 +0,0 @@ -class @GroupAvatar - constructor: -> - $('.js-choose-group-avatar-button').bind "click", -> - form = $(this).closest("form") - form.find(".js-group-avatar-input").click() - $('.js-group-avatar-input').bind "change", -> - form = $(this).closest("form") - filename = $(this).val().replace(/^.*[\\\/]/, '') - form.find(".js-avatar-filename").text(filename) diff --git a/app/assets/javascripts/groups.js b/app/assets/javascripts/groups.js new file mode 100644 index 00000000000..4382dd6860f --- /dev/null +++ b/app/assets/javascripts/groups.js @@ -0,0 +1,13 @@ +(function() { + this.GroupMembers = (function() { + function GroupMembers() { + $('li.group_member').bind('ajax:success', function() { + return $(this).fadeOut(); + }); + } + + return GroupMembers; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/groups.js.coffee b/app/assets/javascripts/groups.js.coffee deleted file mode 100644 index cc905e91ea2..00000000000 --- a/app/assets/javascripts/groups.js.coffee +++ /dev/null @@ -1,4 +0,0 @@ -class @GroupMembers - constructor: -> - $('li.group_member').bind 'ajax:success', -> - $(this).fadeOut() diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js new file mode 100644 index 00000000000..fd5b6dc0ddd --- /dev/null +++ b/app/assets/javascripts/groups_select.js @@ -0,0 +1,67 @@ +(function() { + var slice = [].slice; + + this.GroupsSelect = (function() { + function GroupsSelect() { + $('.ajax-groups-select').each((function(_this) { + return function(i, select) { + var skip_ldap; + skip_ldap = $(select).hasClass('skip_ldap'); + return $(select).select2({ + placeholder: "Search for a group", + multiple: $(select).hasClass('multiselect'), + minimumInputLength: 0, + query: function(query) { + return Api.groups(query.term, skip_ldap, function(groups) { + var data; + data = { + results: groups + }; + return query.callback(data); + }); + }, + initSelection: function(element, callback) { + var id; + id = $(element).val(); + if (id !== "") { + return Api.group(id, callback); + } + }, + formatResult: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return _this.formatResult.apply(_this, args); + }, + formatSelection: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return _this.formatSelection.apply(_this, args); + }, + dropdownCssClass: "ajax-groups-dropdown", + escapeMarkup: function(m) { + return m; + } + }); + }; + })(this)); + } + + GroupsSelect.prototype.formatResult = function(group) { + var avatar; + if (group.avatar_url) { + avatar = group.avatar_url; + } else { + avatar = gon.default_avatar_url; + } + return "<div class='group-result'> <div class='group-name'>" + group.name + "</div> <div class='group-path'>" + group.path + "</div> </div>"; + }; + + GroupsSelect.prototype.formatSelection = function(group) { + return group.name; + }; + + return GroupsSelect; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/groups_select.js.coffee b/app/assets/javascripts/groups_select.js.coffee deleted file mode 100644 index 1084e2a17d1..00000000000 --- a/app/assets/javascripts/groups_select.js.coffee +++ /dev/null @@ -1,41 +0,0 @@ -class @GroupsSelect - constructor: -> - $('.ajax-groups-select').each (i, select) => - skip_ldap = $(select).hasClass('skip_ldap') - - $(select).select2 - placeholder: "Search for a group" - multiple: $(select).hasClass('multiselect') - minimumInputLength: 0 - query: (query) -> - Api.groups query.term, skip_ldap, (groups) -> - data = { results: groups } - query.callback(data) - - initSelection: (element, callback) -> - id = $(element).val() - if id isnt "" - Api.group(id, callback) - - - formatResult: (args...) => - @formatResult(args...) - formatSelection: (args...) => - @formatSelection(args...) - dropdownCssClass: "ajax-groups-dropdown" - escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results - m - - formatResult: (group) -> - if group.avatar_url - avatar = group.avatar_url - else - avatar = gon.default_avatar_url - - "<div class='group-result'> - <div class='group-name'>#{group.name}</div> - <div class='group-path'>#{group.path}</div> - </div>" - - formatSelection: (group) -> - group.name diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js new file mode 100644 index 00000000000..0f840821f53 --- /dev/null +++ b/app/assets/javascripts/importer_status.js @@ -0,0 +1,77 @@ +(function() { + this.ImporterStatus = (function() { + function ImporterStatus(jobs_url, import_url) { + this.jobs_url = jobs_url; + this.import_url = import_url; + this.initStatusPage(); + this.setAutoUpdate(); + } + + ImporterStatus.prototype.initStatusPage = function() { + $('.js-add-to-import').off('click').on('click', (function(_this) { + return function(e) { + var $btn, $namespace_input, $target_field, $tr, id, new_namespace; + $btn = $(e.currentTarget); + $tr = $btn.closest('tr'); + $target_field = $tr.find('.import-target'); + $namespace_input = $target_field.find('input'); + id = $tr.attr('id').replace('repo_', ''); + new_namespace = null; + if ($namespace_input.length > 0) { + new_namespace = $namespace_input.prop('value'); + $target_field.empty().append(new_namespace + "/" + ($target_field.data('project_name'))); + } + $btn.disable().addClass('is-loading'); + return $.post(_this.import_url, { + repo_id: id, + new_namespace: new_namespace + }, { + dataType: 'script' + }); + }; + })(this)); + return $('.js-import-all').off('click').on('click', function(e) { + var $btn; + $btn = $(this); + $btn.disable().addClass('is-loading'); + return $('.js-add-to-import').each(function() { + return $(this).trigger('click'); + }); + }); + }; + + ImporterStatus.prototype.setAutoUpdate = function() { + return setInterval(((function(_this) { + return function() { + return $.get(_this.jobs_url, function(data) { + return $.each(data, function(i, job) { + var job_item, status_field; + job_item = $("#project_" + job.id); + status_field = job_item.find(".job-status"); + if (job.import_status === 'finished') { + job_item.removeClass("active").addClass("success"); + return status_field.html('<span><i class="fa fa-check"></i> done</span>'); + } else if (job.import_status === 'started') { + return status_field.html("<i class='fa fa-spinner fa-spin'></i> started"); + } else { + return status_field.html(job.import_status); + } + }); + }); + }; + })(this)), 4000); + }; + + return ImporterStatus; + + })(); + + $(function() { + if ($('.js-importer-status').length) { + var jobsImportPath = $('.js-importer-status').data('jobs-import-path'); + var importPath = $('.js-importer-status').data('import-path'); + + new ImporterStatus(jobsImportPath, importPath); + } + }); +}).call(this); diff --git a/app/assets/javascripts/importer_status.js.coffee b/app/assets/javascripts/importer_status.js.coffee deleted file mode 100644 index eb046eb2eff..00000000000 --- a/app/assets/javascripts/importer_status.js.coffee +++ /dev/null @@ -1,53 +0,0 @@ -class @ImporterStatus - constructor: (@jobs_url, @import_url) -> - this.initStatusPage() - this.setAutoUpdate() - - initStatusPage: -> - $('.js-add-to-import') - .off 'click' - .on 'click', (e) => - $btn = $(e.currentTarget) - $tr = $btn.closest('tr') - $target_field = $tr.find('.import-target') - $namespace_input = $target_field.find('input') - id = $tr.attr('id').replace('repo_', '') - new_namespace = null - - if $namespace_input.length > 0 - new_namespace = $namespace_input.prop('value') - $target_field.empty().append("#{new_namespace}/#{$target_field.data('project_name')}") - - $btn - .disable() - .addClass 'is-loading' - - $.post @import_url, {repo_id: id, new_namespace: new_namespace}, dataType: 'script' - - $('.js-import-all') - .off 'click' - .on 'click', (e) -> - $btn = $(@) - $btn - .disable() - .addClass 'is-loading' - - $('.js-add-to-import').each -> - $(this).trigger('click') - - setAutoUpdate: -> - setInterval (=> - $.get @jobs_url, (data) => - $.each data, (i, job) => - job_item = $("#project_" + job.id) - status_field = job_item.find(".job-status") - - if job.import_status == 'finished' - job_item.removeClass("active").addClass("success") - status_field.html('<span><i class="fa fa-check"></i> done</span>') - else if job.import_status == 'started' - status_field.html("<i class='fa fa-spinner fa-spin'></i> started") - else - status_field.html(job.import_status) - - ), 4000 diff --git a/app/assets/javascripts/issuable.js b/app/assets/javascripts/issuable.js new file mode 100644 index 00000000000..f27f1bad1f7 --- /dev/null +++ b/app/assets/javascripts/issuable.js @@ -0,0 +1,89 @@ +(function() { + var issuable_created; + + issuable_created = false; + + this.Issuable = { + init: function() { + if (!issuable_created) { + issuable_created = true; + Issuable.initTemplates(); + Issuable.initSearch(); + Issuable.initChecks(); + return Issuable.initLabelFilterRemove(); + } + }, + initTemplates: function() { + return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>'); + }, + initSearch: function() { + this.timer = null; + return $('#issue_search').off('keyup').on('keyup', function() { + clearTimeout(this.timer); + return this.timer = setTimeout(function() { + var $form, $input, $search; + $search = $('#issue_search'); + $form = $('.js-filter-form'); + $input = $("input[name='" + ($search.attr('name')) + "']", $form); + if ($input.length === 0) { + $form.append("<input type='hidden' name='" + ($search.attr('name')) + "' value='" + (_.escape($search.val())) + "'/>"); + } else { + $input.val($search.val()); + } + if ($search.val() !== '') { + return Issuable.filterResults($form); + } + }, 500); + }); + }, + initLabelFilterRemove: function() { + return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) { + var $button; + $button = $(this); + $('input[name="label_name[]"]').filter(function() { + return this.value === $button.data('label'); + }).remove(); + Issuable.filterResults($('.filter-form')); + return $('.js-label-select').trigger('update.label'); + }); + }, + filterResults: (function(_this) { + return function(form) { + var formAction, formData, issuesUrl; + formData = form.serialize(); + formAction = form.attr('action'); + issuesUrl = formAction; + issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&'); + issuesUrl += formData; + return Turbolinks.visit(issuesUrl); + }; + })(this), + initChecks: function() { + this.issuableBulkActions = $('.bulk-update').data('bulkActions'); + $('.check_all_issues').off('click').on('click', function() { + $('.selected_issue').prop('checked', this.checked); + return Issuable.checkChanged(); + }); + return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this)); + }, + checkChanged: function() { + var checked_issues, ids; + checked_issues = $('.selected_issue:checked'); + if (checked_issues.length > 0) { + ids = $.map(checked_issues, function(value) { + return $(value).data('id'); + }); + $('#update_issues_ids').val(ids); + $('.issues-other-filters').hide(); + $('.issues_bulk_update').show(); + } else { + $('#update_issues_ids').val([]); + $('.issues_bulk_update').hide(); + $('.issues-other-filters').show(); + this.issuableBulkActions.willUpdateLabels = false; + } + return true; + } + }; + +}).call(this); diff --git a/app/assets/javascripts/issuable.js.coffee b/app/assets/javascripts/issuable.js.coffee deleted file mode 100644 index 7f795f8096b..00000000000 --- a/app/assets/javascripts/issuable.js.coffee +++ /dev/null @@ -1,93 +0,0 @@ -issuable_created = false -@Issuable = - init: -> - unless issuable_created - issuable_created = true - Issuable.initTemplates() - Issuable.initSearch() - Issuable.initChecks() - Issuable.initLabelFilterRemove() - - initTemplates: -> - Issuable.labelRow = _.template( - '<% _.each(labels, function(label){ %> - <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> - <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> - <%- label.title %> - </a> - <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> - <i class="fa fa-times"></i> - </button> - </span> - <% }); %>' - ) - - initSearch: -> - @timer = null - $('#issue_search') - .off 'keyup' - .on 'keyup', -> - clearTimeout(@timer) - @timer = setTimeout( -> - $search = $('#issue_search') - $form = $('.js-filter-form') - $input = $("input[name='#{$search.attr('name')}']", $form) - if $input.length is 0 - $form.append "<input type='hidden' name='#{$search.attr('name')}' value='#{_.escape($search.val())}'/>" - else - $input.val $search.val() - Issuable.filterResults $form if $search.val() isnt '' - , 500) - - initLabelFilterRemove: -> - $(document) - .off 'click', '.js-label-filter-remove' - .on 'click', '.js-label-filter-remove', (e) -> - $button = $(@) - - # Remove the label input box - $('input[name="label_name[]"]') - .filter -> @value is $button.data('label') - .remove() - - # Submit the form to get new data - Issuable.filterResults $('.filter-form') - $('.js-label-select').trigger('update.label') - - filterResults: (form) => - formData = form.serialize() - - formAction = form.attr('action') - issuesUrl = formAction - issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}") - issuesUrl += formData - - Turbolinks.visit(issuesUrl) - - initChecks: -> - @issuableBulkActions = $('.bulk-update').data('bulkActions') - - $('.check_all_issues').off('click').on('click', -> - $('.selected_issue').prop('checked', @checked) - Issuable.checkChanged() - ) - - $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(@)) - - - checkChanged: -> - checked_issues = $('.selected_issue:checked') - if checked_issues.length > 0 - ids = $.map checked_issues, (value) -> - $(value).data('id') - - $('#update_issues_ids').val ids - $('.issues-other-filters').hide() - $('.issues_bulk_update').show() - else - $('#update_issues_ids').val [] - $('.issues_bulk_update').hide() - $('.issues-other-filters').show() - @issuableBulkActions.willUpdateLabels = false - - return true diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js new file mode 100644 index 00000000000..8147e83ffe8 --- /dev/null +++ b/app/assets/javascripts/issuable_context.js @@ -0,0 +1,69 @@ +(function() { + this.IssuableContext = (function() { + function IssuableContext(currentUser) { + this.initParticipants(); + new UsersSelect(currentUser); + $('select.select2').select2({ + width: 'resolve', + dropdownAutoWidth: true + }); + $(".issuable-sidebar .inline-update").on("change", "select", function() { + return $(this).submit(); + }); + $(".issuable-sidebar .inline-update").on("change", ".js-assignee", function() { + return $(this).submit(); + }); + $(document).off('click', '.issuable-sidebar .dropdown-content a').on('click', '.issuable-sidebar .dropdown-content a', function(e) { + return e.preventDefault(); + }); + $(document).off('click', '.edit-link').on('click', '.edit-link', function(e) { + var $block, $selectbox; + e.preventDefault(); + $block = $(this).parents('.block'); + $selectbox = $block.find('.selectbox'); + if ($selectbox.is(':visible')) { + $selectbox.hide(); + $block.find('.value').show(); + } else { + $selectbox.show(); + $block.find('.value').hide(); + } + if ($selectbox.is(':visible')) { + return setTimeout(function() { + return $block.find('.dropdown-menu-toggle').trigger('click'); + }, 0); + } + }); + $(".right-sidebar").niceScroll(); + } + + IssuableContext.prototype.initParticipants = function() { + var _this; + _this = this; + $(document).on("click", ".js-participants-more", this.toggleHiddenParticipants); + return $(".js-participants-author").each(function(i) { + if (i >= _this.PARTICIPANTS_ROW_COUNT) { + return $(this).addClass("js-participants-hidden").hide(); + } + }); + }; + + IssuableContext.prototype.toggleHiddenParticipants = function(e) { + var currentText, lessText, originalText; + e.preventDefault(); + currentText = $(this).text().trim(); + lessText = $(this).data("less-text"); + originalText = $(this).data("original-text"); + if (currentText === originalText) { + $(this).text(lessText); + } else { + $(this).text(originalText); + } + return $(".js-participants-hidden").toggle(); + }; + + return IssuableContext; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee deleted file mode 100644 index 3c491ebfc4c..00000000000 --- a/app/assets/javascripts/issuable_context.js.coffee +++ /dev/null @@ -1,60 +0,0 @@ -class @IssuableContext - constructor: (currentUser) -> - @initParticipants() - new UsersSelect(currentUser) - $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}) - - $(".issuable-sidebar .inline-update").on "change", "select", -> - $(this).submit() - $(".issuable-sidebar .inline-update").on "change", ".js-assignee", -> - $(this).submit() - - $(document) - .off 'click', '.issuable-sidebar .dropdown-content a' - .on 'click', '.issuable-sidebar .dropdown-content a', (e) -> - e.preventDefault() - - $(document) - .off 'click', '.edit-link' - .on 'click', '.edit-link', (e) -> - e.preventDefault() - - $block = $(@).parents('.block') - $selectbox = $block.find('.selectbox') - if $selectbox.is(':visible') - $selectbox.hide() - $block.find('.value').show() - else - $selectbox.show() - $block.find('.value').hide() - - if $selectbox.is(':visible') - setTimeout -> - $block.find('.dropdown-menu-toggle').trigger 'click' - , 0 - - $(".right-sidebar").niceScroll() - - initParticipants: -> - _this = @ - $(document).on "click", ".js-participants-more", @toggleHiddenParticipants - - $(".js-participants-author").each (i) -> - if i >= _this.PARTICIPANTS_ROW_COUNT - $(@) - .addClass "js-participants-hidden" - .hide() - - toggleHiddenParticipants: (e) -> - e.preventDefault() - - currentText = $(this).text().trim() - lessText = $(this).data("less-text") - originalText = $(this).data("original-text") - - if currentText is originalText - $(this).text(lessText) - else - $(this).text(originalText) - - $(".js-participants-hidden").toggle() diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js new file mode 100644 index 00000000000..297d4f029f0 --- /dev/null +++ b/app/assets/javascripts/issuable_form.js @@ -0,0 +1,136 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.IssuableForm = (function() { + IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?'; + + IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i; + + function IssuableForm(form) { + var $issuableDueDate; + this.form = form; + this.toggleWip = bind(this.toggleWip, this); + this.renderWipExplanation = bind(this.renderWipExplanation, this); + this.resetAutosave = bind(this.resetAutosave, this); + this.handleSubmit = bind(this.handleSubmit, this); + GitLab.GfmAutoComplete.setup(); + new UsersSelect(); + new ZenMode(); + this.titleField = this.form.find("input[name*='[title]']"); + this.descriptionField = this.form.find("textarea[name*='[description]']"); + this.issueMoveField = this.form.find("#move_to_project_id"); + if (!(this.titleField.length && this.descriptionField.length)) { + return; + } + this.initAutosave(); + this.form.on("submit", this.handleSubmit); + this.form.on("click", ".btn-cancel", this.resetAutosave); + this.initWip(); + this.initMoveDropdown(); + $issuableDueDate = $('#issuable-due-date'); + if ($issuableDueDate.length) { + $('.datepicker').datepicker({ + dateFormat: 'yy-mm-dd', + onSelect: function(dateText, inst) { + return $issuableDueDate.val(dateText); + } + }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $issuableDueDate.val())); + } + } + + IssuableForm.prototype.initAutosave = function() { + new Autosave(this.titleField, [document.location.pathname, document.location.search, "title"]); + return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, "description"]); + }; + + IssuableForm.prototype.handleSubmit = function() { + var ref, ref1; + if (((ref = parseInt((ref1 = this.issueMoveField) != null ? ref1.val() : void 0)) != null ? ref : 0) > 0) { + if (!confirm(this.issueMoveConfirmMsg)) { + return false; + } + } + return this.resetAutosave(); + }; + + IssuableForm.prototype.resetAutosave = function() { + this.titleField.data("autosave").reset(); + return this.descriptionField.data("autosave").reset(); + }; + + IssuableForm.prototype.initWip = function() { + this.$wipExplanation = this.form.find(".js-wip-explanation"); + this.$noWipExplanation = this.form.find(".js-no-wip-explanation"); + if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) { + return; + } + this.form.on("click", ".js-toggle-wip", this.toggleWip); + this.titleField.on("keyup blur", this.renderWipExplanation); + return this.renderWipExplanation(); + }; + + IssuableForm.prototype.workInProgress = function() { + return this.wipRegex.test(this.titleField.val()); + }; + + IssuableForm.prototype.renderWipExplanation = function() { + if (this.workInProgress()) { + this.$wipExplanation.show(); + return this.$noWipExplanation.hide(); + } else { + this.$wipExplanation.hide(); + return this.$noWipExplanation.show(); + } + }; + + IssuableForm.prototype.toggleWip = function(event) { + event.preventDefault(); + if (this.workInProgress()) { + this.removeWip(); + } else { + this.addWip(); + } + return this.renderWipExplanation(); + }; + + IssuableForm.prototype.removeWip = function() { + return this.titleField.val(this.titleField.val().replace(this.wipRegex, "")); + }; + + IssuableForm.prototype.addWip = function() { + return this.titleField.val("WIP: " + (this.titleField.val())); + }; + + IssuableForm.prototype.initMoveDropdown = function() { + var $moveDropdown; + $moveDropdown = $('.js-move-dropdown'); + if ($moveDropdown.length) { + return $('.js-move-dropdown').select2({ + ajax: { + url: $moveDropdown.data('projects-url'), + results: function(data) { + return { + results: data + }; + }, + data: function(query) { + return { + search: query + }; + } + }, + formatResult: function(project) { + return project.name_with_namespace; + }, + formatSelection: function(project) { + return project.name_with_namespace; + } + }); + } + }; + + return IssuableForm; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee deleted file mode 100644 index 5b7a4831dfc..00000000000 --- a/app/assets/javascripts/issuable_form.js.coffee +++ /dev/null @@ -1,112 +0,0 @@ -class @IssuableForm - issueMoveConfirmMsg: 'Are you sure you want to move this issue to another project?' - wipRegex: /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i - - constructor: (@form) -> - GitLab.GfmAutoComplete.setup() - new UsersSelect() - new ZenMode() - - @titleField = @form.find("input[name*='[title]']") - @descriptionField = @form.find("textarea[name*='[description]']") - @issueMoveField = @form.find("#move_to_project_id") - - return unless @titleField.length && @descriptionField.length - - @initAutosave() - - @form.on "submit", @handleSubmit - @form.on "click", ".btn-cancel", @resetAutosave - - @initWip() - @initMoveDropdown() - - $issuableDueDate = $('#issuable-due-date') - - if $issuableDueDate.length - $('.datepicker').datepicker( - dateFormat: 'yy-mm-dd', - onSelect: (dateText, inst) -> - $issuableDueDate.val dateText - ).datepicker 'setDate', $.datepicker.parseDate('yy-mm-dd', $issuableDueDate.val()) - - initAutosave: -> - new Autosave @titleField, [ - document.location.pathname, - document.location.search, - "title" - ] - - new Autosave @descriptionField, [ - document.location.pathname, - document.location.search, - "description" - ] - - handleSubmit: => - if (parseInt(@issueMoveField?.val()) ? 0) > 0 - return false unless confirm(@issueMoveConfirmMsg) - - @resetAutosave() - - resetAutosave: => - @titleField.data("autosave").reset() - @descriptionField.data("autosave").reset() - - initWip: -> - @$wipExplanation = @form.find(".js-wip-explanation") - @$noWipExplanation = @form.find(".js-no-wip-explanation") - return unless @$wipExplanation.length and @$noWipExplanation.length - - @form.on "click", ".js-toggle-wip", @toggleWip - - @titleField.on "keyup blur", @renderWipExplanation - - @renderWipExplanation() - - workInProgress: -> - @wipRegex.test @titleField.val() - - renderWipExplanation: => - if @workInProgress() - @$wipExplanation.show() - @$noWipExplanation.hide() - else - @$wipExplanation.hide() - @$noWipExplanation.show() - - toggleWip: (event) => - event.preventDefault() - - if @workInProgress() - @removeWip() - else - @addWip() - - @renderWipExplanation() - - removeWip: -> - @titleField.val @titleField.val().replace(@wipRegex, "") - - addWip: -> - @titleField.val "WIP: #{@titleField.val()}" - - initMoveDropdown: -> - $moveDropdown = $('.js-move-dropdown') - - if $moveDropdown.length - $('.js-move-dropdown').select2 - ajax: - url: $moveDropdown.data('projects-url') - results: (data) -> - return { - results: data - } - data: (query) -> - { - search: query - } - formatResult: (project) -> - project.name_with_namespace - formatSelection: (project) -> - project.name_with_namespace diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js new file mode 100644 index 00000000000..6838d9d8da1 --- /dev/null +++ b/app/assets/javascripts/issue.js @@ -0,0 +1,154 @@ + +/*= require flash */ + + +/*= require jquery.waitforimages */ + + +/*= require task_list */ + +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Issue = (function() { + function Issue() { + this.submitNoteForm = bind(this.submitNoteForm, this); + this.disableTaskList(); + if ($('a.btn-close').length) { + this.initTaskList(); + this.initIssueBtnEventListeners(); + } + this.initMergeRequests(); + this.initRelatedBranches(); + 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; + issueFailMessage = 'Unable to update this issue at this time.'; + return $('a.btn-close, a.btn-reopen').on('click', function(e) { + var $this, isClose, shouldSubmit, url; + e.preventDefault(); + e.stopImmediatePropagation(); + $this = $(this); + isClose = $this.hasClass('btn-close'); + shouldSubmit = $this.hasClass('btn-comment'); + if (shouldSubmit) { + _this.submitNoteForm($this.closest('form')); + } + $this.prop('disabled', true); + url = $this.attr('href'); + return $.ajax({ + type: 'PUT', + url: url, + error: function(jqXHR, textStatus, errorThrown) { + var issueStatus; + issueStatus = isClose ? 'close' : 'open'; + return new Flash(issueFailMessage, 'alert'); + }, + success: function(data, textStatus, jqXHR) { + if ('id' in data) { + $(document).trigger('issuable:change'); + if (isClose) { + $('a.btn-close').addClass('hidden'); + $('a.btn-reopen').removeClass('hidden'); + $('div.status-box-closed').removeClass('hidden'); + $('div.status-box-open').addClass('hidden'); + } else { + $('a.btn-reopen').addClass('hidden'); + $('a.btn-close').removeClass('hidden'); + $('div.status-box-closed').addClass('hidden'); + $('div.status-box-open').removeClass('hidden'); + } + } else { + new Flash(issueFailMessage, 'alert'); + } + return $this.prop('disabled', false); + } + }); + }); + }; + + Issue.prototype.submitNoteForm = function(form) { + var noteText; + noteText = form.find("textarea.js-note-text").val(); + if (noteText.trim().length > 0) { + return form.submit(); + } + }; + + 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 + }); + }; + + Issue.prototype.initMergeRequests = function() { + var $container; + $container = $('#merge-requests'); + return $.getJSON($container.data('url')).error(function() { + return new Flash('Failed to load referenced merge requests', 'alert'); + }).success(function(data) { + if ('html' in data) { + return $container.html(data.html); + } + }); + }; + + Issue.prototype.initRelatedBranches = function() { + var $container; + $container = $('#related-branches'); + return $.getJSON($container.data('url')).error(function() { + return new Flash('Failed to load related branches', 'alert'); + }).success(function(data) { + if ('html' in data) { + return $container.html(data.html); + } + }); + }; + + Issue.prototype.initCanCreateBranch = function() { + var $container; + $container = $('div#new-branch'); + if ($container.length === 0) { + return; + } + return $.getJSON($container.data('path')).error(function() { + $container.find('.checking').hide(); + $container.find('.unavailable').show(); + return new Flash('Failed to check if a new branch can be created.', 'alert'); + }).success(function(data) { + if (data.can_create_branch) { + $container.find('.checking').hide(); + $container.find('.available').show(); + return $container.find('a').attr('disabled', false); + } else { + $container.find('.checking').hide(); + return $container.find('.unavailable').show(); + } + }); + }; + + return Issue; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee deleted file mode 100644 index f446aa49cde..00000000000 --- a/app/assets/javascripts/issue.js.coffee +++ /dev/null @@ -1,117 +0,0 @@ -#= require flash -#= require jquery.waitforimages -#= require task_list - -class @Issue - constructor: -> - # Prevent duplicate event bindings - @disableTaskList() - if $('a.btn-close').length - @initTaskList() - @initIssueBtnEventListeners() - - @initMergeRequests() - @initRelatedBranches() - @initCanCreateBranch() - - initTaskList: -> - $('.detail-page-description .js-task-list-container').taskList('enable') - $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList - - initIssueBtnEventListeners: -> - _this = @ - issueFailMessage = 'Unable to update this issue at this time.' - $('a.btn-close, a.btn-reopen').on 'click', (e) -> - e.preventDefault() - e.stopImmediatePropagation() - $this = $(this) - isClose = $this.hasClass('btn-close') - shouldSubmit = $this.hasClass('btn-comment') - if shouldSubmit - _this.submitNoteForm($this.closest('form')) - $this.prop('disabled', true) - url = $this.attr('href') - $.ajax - type: 'PUT' - url: url, - error: (jqXHR, textStatus, errorThrown) -> - issueStatus = if isClose then 'close' else 'open' - new Flash(issueFailMessage, 'alert') - success: (data, textStatus, jqXHR) -> - if 'id' of data - $(document).trigger('issuable:change'); - if isClose - $('a.btn-close').addClass('hidden') - $('a.btn-reopen').removeClass('hidden') - $('div.status-box-closed').removeClass('hidden') - $('div.status-box-open').addClass('hidden') - else - $('a.btn-reopen').addClass('hidden') - $('a.btn-close').removeClass('hidden') - $('div.status-box-closed').addClass('hidden') - $('div.status-box-open').removeClass('hidden') - else - new Flash(issueFailMessage, 'alert') - $this.prop('disabled', false) - - submitNoteForm: (form) => - noteText = form.find("textarea.js-note-text").val() - if noteText.trim().length > 0 - form.submit() - - disableTaskList: -> - $('.detail-page-description .js-task-list-container').taskList('disable') - $(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container' - - # TODO (rspeicher): Make the issue description inline-editable like a note so - # that we can re-use its form here - updateTaskList: -> - patchData = {} - patchData['issue'] = {'description': $('.js-task-list-field', this).val()} - - $.ajax - type: 'PATCH' - url: $('form.js-issuable-update').attr('action') - data: patchData - - initMergeRequests: -> - $container = $('#merge-requests') - - $.getJSON($container.data('url')) - .error -> - new Flash('Failed to load referenced merge requests', 'alert') - .success (data) -> - if 'html' of data - $container.html(data.html) - - initRelatedBranches: -> - $container = $('#related-branches') - - $.getJSON($container.data('url')) - .error -> - new Flash('Failed to load related branches', 'alert') - .success (data) -> - if 'html' of data - $container.html(data.html) - - initCanCreateBranch: -> - $container = $('div#new-branch') - - # If the user doesn't have the required permissions the container isn't - # rendered at all. - return if $container.length is 0 - - $.getJSON($container.data('path')) - .error -> - $container.find('.checking').hide() - $container.find('.unavailable').show() - - new Flash('Failed to check if a new branch can be created.', 'alert') - .success (data) -> - if data.can_create_branch - $container.find('.checking').hide() - $container.find('.available').show() - $container.find('a').attr('disabled', false) - else - $container.find('.checking').hide() - $container.find('.unavailable').show() diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js new file mode 100644 index 00000000000..076e3972944 --- /dev/null +++ b/app/assets/javascripts/issue_status_select.js @@ -0,0 +1,35 @@ +(function() { + this.IssueStatusSelect = (function() { + function IssueStatusSelect() { + $('.js-issue-status').each(function(i, el) { + var fieldName; + fieldName = $(el).data("field-name"); + return $(el).glDropdown({ + selectable: true, + fieldName: fieldName, + toggleLabel: (function(_this) { + return function(selected, el, instance) { + var $item, label; + label = 'Author'; + $item = instance.dropdown.find('.is-active'); + if ($item.length) { + label = $item.text(); + } + return label; + }; + })(this), + clicked: function(item, $el, e) { + return e.preventDefault(); + }, + id: function(obj, el) { + return $(el).data("id"); + } + }); + }); + } + + return IssueStatusSelect; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/issue_status_select.js.coffee b/app/assets/javascripts/issue_status_select.js.coffee deleted file mode 100644 index ed50e2e698f..00000000000 --- a/app/assets/javascripts/issue_status_select.js.coffee +++ /dev/null @@ -1,18 +0,0 @@ -class @IssueStatusSelect - constructor: -> - $('.js-issue-status').each (i, el) -> - fieldName = $(el).data("field-name") - - $(el).glDropdown( - selectable: true - fieldName: fieldName - toggleLabel: (selected, el, instance) => - label = 'Author' - $item = instance.dropdown.find('.is-active') - label = $item.text() if $item.length - label - clicked: (item, $el, e)-> - e.preventDefault() - id: (obj, el) -> - $(el).data("id") - ) diff --git a/app/assets/javascripts/issues-bulk-assignment.js b/app/assets/javascripts/issues-bulk-assignment.js new file mode 100644 index 00000000000..98d3358ba92 --- /dev/null +++ b/app/assets/javascripts/issues-bulk-assignment.js @@ -0,0 +1,161 @@ +(function() { + this.IssuableBulkActions = (function() { + function IssuableBulkActions(opts) { + var ref, ref1, ref2; + if (opts == null) { + opts = {}; + } + this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issues-list .issue'); + this.form.data('bulkActions', this); + this.willUpdateLabels = false; + this.bindEvents(); + Issuable.initChecks(); + } + + IssuableBulkActions.prototype.getElement = function(selector) { + return this.container.find(selector); + }; + + IssuableBulkActions.prototype.bindEvents = function() { + return this.form.off('submit').on('submit', this.onFormSubmit.bind(this)); + }; + + IssuableBulkActions.prototype.onFormSubmit = function(e) { + e.preventDefault(); + return this.submit(); + }; + + IssuableBulkActions.prototype.submit = function() { + var _this, xhr; + _this = this; + xhr = $.ajax({ + url: this.form.attr('action'), + method: this.form.attr('method'), + dataType: 'JSON', + data: this.getFormDataAsObject() + }); + xhr.done(function(response, status, xhr) { + return location.reload(); + }); + xhr.fail(function() { + return new Flash("Issue update failed"); + }); + return xhr.always(this.onFormSubmitAlways.bind(this)); + }; + + IssuableBulkActions.prototype.onFormSubmitAlways = function() { + return this.form.find('[type="submit"]').enable(); + }; + + IssuableBulkActions.prototype.getSelectedIssues = function() { + return this.issues.has('.selected_issue:checked'); + }; + + IssuableBulkActions.prototype.getLabelsFromSelection = function() { + var labels; + labels = []; + this.getSelectedIssues().map(function() { + var _labels; + _labels = $(this).data('labels'); + if (_labels) { + return _labels.map(function(labelId) { + if (labels.indexOf(labelId) === -1) { + return labels.push(labelId); + } + }); + } + }); + return labels; + }; + + + /** + * Will return only labels that were marked previously and the user has unmarked + * @return {Array} Label IDs + */ + + IssuableBulkActions.prototype.getUnmarkedIndeterminedLabels = function() { + var el, i, id, j, labelsToKeep, len, len1, ref, ref1, result; + result = []; + labelsToKeep = []; + ref = this.getElement('.labels-filter .is-indeterminate'); + for (i = 0, len = ref.length; i < len; i++) { + el = ref[i]; + labelsToKeep.push($(el).data('labelId')); + } + ref1 = this.getLabelsFromSelection(); + for (j = 0, len1 = ref1.length; j < len1; j++) { + id = ref1[j]; + if (labelsToKeep.indexOf(id) === -1) { + result.push(id); + } + } + return result; + }; + + + /** + * Simple form serialization, it will return just what we need + * Returns key/value pairs from form data + */ + + IssuableBulkActions.prototype.getFormDataAsObject = function() { + var formData; + formData = { + update: { + state_event: this.form.find('input[name="update[state_event]"]').val(), + assignee_id: this.form.find('input[name="update[assignee_id]"]').val(), + milestone_id: this.form.find('input[name="update[milestone_id]"]').val(), + issues_ids: this.form.find('input[name="update[issues_ids]"]').val(), + subscription_event: this.form.find('input[name="update[subscription_event]"]').val(), + add_label_ids: [], + remove_label_ids: [] + } + }; + if (this.willUpdateLabels) { + this.getLabelsToApply().map(function(id) { + return formData.update.add_label_ids.push(id); + }); + this.getLabelsToRemove().map(function(id) { + return formData.update.remove_label_ids.push(id); + }); + } + return formData; + }; + + IssuableBulkActions.prototype.getLabelsToApply = function() { + var $labels, labelIds; + labelIds = []; + $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]'); + $labels.each(function(k, label) { + if (label) { + return labelIds.push(parseInt($(label).val())); + } + }); + return labelIds; + }; + + + /** + * Returns Label IDs that will be removed from issue selection + * @return {Array} Array of labels IDs + */ + + IssuableBulkActions.prototype.getLabelsToRemove = function() { + var indeterminatedLabels, labelsToApply, result; + result = []; + indeterminatedLabels = this.getUnmarkedIndeterminedLabels(); + labelsToApply = this.getLabelsToApply(); + indeterminatedLabels.map(function(id) { + if (labelsToApply.indexOf(id) === -1) { + return result.push(id); + } + }); + return result; + }; + + return IssuableBulkActions; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/issues-bulk-assignment.js.coffee b/app/assets/javascripts/issues-bulk-assignment.js.coffee deleted file mode 100644 index 3d09ea08e3b..00000000000 --- a/app/assets/javascripts/issues-bulk-assignment.js.coffee +++ /dev/null @@ -1,128 +0,0 @@ -class @IssuableBulkActions - constructor: (opts = {}) -> - # Set defaults - { - @container = $('.content') - @form = @getElement('.bulk-update') - @issues = @getElement('.issues-list .issue') - } = opts - - # Save instance - @form.data 'bulkActions', @ - - @willUpdateLabels = false - - @bindEvents() - - # Fixes bulk-assign not working when navigating through pages - Issuable.initChecks(); - - getElement: (selector) -> - @container.find selector - - bindEvents: -> - @form.off('submit').on('submit', @onFormSubmit.bind(@)) - - onFormSubmit: (e) -> - e.preventDefault() - @submit() - - submit: -> - _this = @ - - xhr = $.ajax - url: @form.attr 'action' - method: @form.attr 'method' - dataType: 'JSON', - data: @getFormDataAsObject() - - xhr.done (response, status, xhr) -> - location.reload() - - xhr.fail -> - new Flash("Issue update failed") - - xhr.always @onFormSubmitAlways.bind(@) - - onFormSubmitAlways: -> - @form.find('[type="submit"]').enable() - - getSelectedIssues: -> - @issues.has('.selected_issue:checked') - - getLabelsFromSelection: -> - labels = [] - - @getSelectedIssues().map -> - _labels = $(@).data('labels') - if _labels - _labels.map (labelId) -> - labels.push(labelId) if labels.indexOf(labelId) is -1 - - labels - - ###* - * Will return only labels that were marked previously and the user has unmarked - * @return {Array} Label IDs - ### - getUnmarkedIndeterminedLabels: -> - result = [] - labelsToKeep = [] - - for el in @getElement('.labels-filter .is-indeterminate') - labelsToKeep.push $(el).data('labelId') - - for id in @getLabelsFromSelection() - # Only the ones that we are not going to keep - result.push(id) if labelsToKeep.indexOf(id) is -1 - - result - - ###* - * Simple form serialization, it will return just what we need - * Returns key/value pairs from form data - ### - getFormDataAsObject: -> - formData = - update: - state_event : @form.find('input[name="update[state_event]"]').val() - assignee_id : @form.find('input[name="update[assignee_id]"]').val() - milestone_id : @form.find('input[name="update[milestone_id]"]').val() - issues_ids : @form.find('input[name="update[issues_ids]"]').val() - subscription_event : @form.find('input[name="update[subscription_event]"]').val() - add_label_ids : [] - remove_label_ids : [] - - if @willUpdateLabels - @getLabelsToApply().map (id) -> - formData.update.add_label_ids.push id - - @getLabelsToRemove().map (id) -> - formData.update.remove_label_ids.push id - - formData - - getLabelsToApply: -> - labelIds = [] - $labels = @form.find('.labels-filter input[name="update[label_ids][]"]') - - $labels.each (k, label) -> - labelIds.push parseInt($(label).val()) if label - - labelIds - - ###* - * Returns Label IDs that will be removed from issue selection - * @return {Array} Array of labels IDs - ### - getLabelsToRemove: -> - result = [] - indeterminatedLabels = @getUnmarkedIndeterminedLabels() - labelsToApply = @getLabelsToApply() - - indeterminatedLabels.map (id) -> - # We need to exclude label IDs that will be applied - # By not doing this will cause issues from selection to not add labels at all - result.push(id) if labelsToApply.indexOf(id) is -1 - - result diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js new file mode 100644 index 00000000000..fe071fca67c --- /dev/null +++ b/app/assets/javascripts/labels.js @@ -0,0 +1,44 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Labels = (function() { + function Labels() { + this.setSuggestedColor = bind(this.setSuggestedColor, this); + this.updateColorPreview = bind(this.updateColorPreview, this); + var form; + form = $('.label-form'); + this.cleanBinding(); + this.addBinding(); + this.updateColorPreview(); + } + + Labels.prototype.addBinding = function() { + $(document).on('click', '.suggest-colors a', this.setSuggestedColor); + return $(document).on('input', 'input#label_color', this.updateColorPreview); + }; + + Labels.prototype.cleanBinding = function() { + $(document).off('click', '.suggest-colors a'); + return $(document).off('input', 'input#label_color'); + }; + + Labels.prototype.updateColorPreview = function() { + var previewColor; + previewColor = $('input#label_color').val(); + return $('div.label-color-preview').css('background-color', previewColor); + }; + + Labels.prototype.setSuggestedColor = function(e) { + var color; + color = $(e.currentTarget).data('color'); + $('input#label_color').val(color); + this.updateColorPreview(); + $('.label-form').trigger('keyup'); + return e.preventDefault(); + }; + + return Labels; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/labels.js.coffee b/app/assets/javascripts/labels.js.coffee deleted file mode 100644 index d05bacd7494..00000000000 --- a/app/assets/javascripts/labels.js.coffee +++ /dev/null @@ -1,28 +0,0 @@ -class @Labels - constructor: -> - form = $('.label-form') - @cleanBinding() - @addBinding() - @updateColorPreview() - - addBinding: -> - $(document).on 'click', '.suggest-colors a', @setSuggestedColor - $(document).on 'input', 'input#label_color', @updateColorPreview - - cleanBinding: -> - $(document).off 'click', '.suggest-colors a' - $(document).off 'input', 'input#label_color' - - # Updates the the preview color with the hex-color input - updateColorPreview: => - previewColor = $('input#label_color').val() - $('div.label-color-preview').css('background-color', previewColor) - - # Updates the preview color with a click on a suggested color - setSuggestedColor: (e) => - color = $(e.currentTarget).data('color') - $('input#label_color').val(color) - @updateColorPreview() - # Notify the form, that color has changed - $('.label-form').trigger('keyup') - e.preventDefault() diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js new file mode 100644 index 00000000000..675dd5b7cea --- /dev/null +++ b/app/assets/javascripts/labels_select.js @@ -0,0 +1,377 @@ +(function() { + this.LabelsSelect = (function() { + function LabelsSelect() { + var _this; + _this = this; + $('.js-label-select').each(function(i, dropdown) { + var $block, $colorPreview, $dropdown, $form, $loading, $newLabelCreateButton, $newLabelError, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, newColorField, newLabelField, projectId, resetForm, saveLabel, saveLabelData, selectedLabel, showAny, showNo; + $dropdown = $(dropdown); + projectId = $dropdown.data('project-id'); + labelUrl = $dropdown.data('labels'); + issueUpdateURL = $dropdown.data('issueUpdate'); + selectedLabel = $dropdown.data('selected'); + if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) { + selectedLabel = selectedLabel.split(','); + } + newLabelField = $('#new_label_name'); + newColorField = $('#new_label_color'); + showNo = $dropdown.data('show-no'); + showAny = $dropdown.data('show-any'); + defaultLabel = $dropdown.data('default-label'); + abilityName = $dropdown.data('ability-name'); + $selectbox = $dropdown.closest('.selectbox'); + $block = $selectbox.closest('.block'); + $form = $dropdown.closest('form'); + $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span'); + $value = $block.find('.value'); + $newLabelError = $('.js-label-error'); + $colorPreview = $('.js-dropdown-label-color-preview'); + $newLabelCreateButton = $('.js-new-label-btn'); + $newLabelError.hide(); + $loading = $block.find('.block-loading').fadeOut(); + if (issueUpdateURL != null) { + issueURLSplit = issueUpdateURL.split('/'); + } + if (issueUpdateURL) { + labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>'); + labelNoneHTMLTemplate = '<span class="no-value">None</span>'; + } + if (newLabelField.length) { + $('.suggest-colors-dropdown a').on("click", function(e) { + e.preventDefault(); + e.stopPropagation(); + newColorField.val($(this).data('color')).trigger('change'); + return $colorPreview.css('background-color', $(this).data('color')).parent().addClass('is-active'); + }); + resetForm = function() { + newLabelField.val('').trigger('change'); + newColorField.val('').trigger('change'); + return $colorPreview.css('background-color', '').parent().removeClass('is-active'); + }; + $('.dropdown-menu-back').on('click', function() { + return resetForm(); + }); + $('.js-cancel-label-btn').on('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + resetForm(); + return $('.dropdown-menu-back', $dropdown.parent()).trigger('click'); + }); + enableLabelCreateButton = function() { + if (newLabelField.val() !== '' && newColorField.val() !== '') { + $newLabelError.hide(); + return $newLabelCreateButton.enable(); + } else { + return $newLabelCreateButton.disable(); + } + }; + saveLabel = function() { + return Api.newLabel(projectId, { + name: newLabelField.val(), + color: newColorField.val() + }, function(label) { + var errors; + $newLabelCreateButton.enable(); + if (label.message != null) { + errors = _.map(label.message, function(value, key) { + return key + " " + value[0]; + }); + return $newLabelError.html(errors.join("<br/>")).show(); + } else { + return $('.dropdown-menu-back', $dropdown.parent()).trigger('click'); + } + }); + }; + newLabelField.on('keyup change', enableLabelCreateButton); + newColorField.on('keyup change', enableLabelCreateButton); + $newLabelCreateButton.disable().on('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + return saveLabel(); + }); + } + saveLabelData = function() { + var data, selected; + selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() { + return this.value; + }).get(); + data = {}; + data[abilityName] = {}; + data[abilityName].label_ids = selected; + if (!selected.length) { + data[abilityName].label_ids = ['']; + } + $loading.fadeIn(); + $dropdown.trigger('loading.gl.dropdown'); + return $.ajax({ + type: 'PUT', + url: issueUpdateURL, + dataType: 'JSON', + data: data + }).done(function(data) { + var labelCount, template; + $loading.fadeOut(); + $dropdown.trigger('loaded.gl.dropdown'); + $selectbox.hide(); + data.issueURLSplit = issueURLSplit; + labelCount = 0; + if (data.labels.length) { + template = labelHTMLTemplate(data); + labelCount = data.labels.length; + } else { + template = labelNoneHTMLTemplate; + } + $value.removeAttr('style').html(template); + $sidebarCollapsedValue.text(labelCount); + $('.has-tooltip', $value).tooltip({ + container: 'body' + }); + return $value.find('a').each(function(i) { + return setTimeout((function(_this) { + return function() { + return gl.animate.animate($(_this), 'pulse'); + }; + })(this), 200 * i); + }); + }); + }; + return $dropdown.glDropdown({ + data: function(term, callback) { + return $.ajax({ + url: labelUrl + }).done(function(data) { + data = _.chain(data).groupBy(function(label) { + return label.title; + }).map(function(label) { + var color; + color = _.map(label, function(dup) { + return dup.color; + }); + return { + id: label[0].id, + title: label[0].title, + color: color, + duplicate: color.length > 1 + }; + }).value(); + if ($dropdown.hasClass('js-extra-options')) { + if (showNo) { + data.unshift({ + id: 0, + title: 'No Label' + }); + } + if (showAny) { + data.unshift({ + isAny: true, + title: 'Any Label' + }); + } + if (data.length > 2) { + data.splice(2, 0, 'divider'); + } + } + return callback(data); + }); + }, + renderRow: function(label, instance) { + var $a, $li, active, color, colorEl, indeterminate, removesAll, selectedClass, spacing; + $li = $('<li>'); + $a = $('<a href="#">'); + selectedClass = []; + removesAll = label.id === 0 || (label.id == null); + if ($dropdown.hasClass('js-filter-bulk-update')) { + indeterminate = instance.indeterminateIds; + active = instance.activeIds; + if (indeterminate.indexOf(label.id) !== -1) { + selectedClass.push('is-indeterminate'); + } + if (active.indexOf(label.id) !== -1) { + i = selectedClass.indexOf('is-indeterminate'); + if (i !== -1) { + selectedClass.splice(i, 1); + } + selectedClass.push('is-active'); + instance.addInput(this.fieldName, label.id); + } + } + if ($form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + (this.id(label)) + "']").length) { + selectedClass.push('is-active'); + } + if ($dropdown.hasClass('js-multiselect') && removesAll) { + selectedClass.push('dropdown-clear-active'); + } + if (label.duplicate) { + spacing = 100 / label.color.length; + label.color = label.color.filter(function(color, i) { + return i < 4; + }); + color = _.map(label.color, function(color, i) { + var percentFirst, percentSecond; + percentFirst = Math.floor(spacing * i); + percentSecond = Math.floor(spacing * (i + 1)); + return color + " " + percentFirst + "%," + color + " " + percentSecond + "% "; + }).join(','); + color = "linear-gradient(" + color + ")"; + } else { + if (label.color != null) { + color = label.color[0]; + } + } + if (color) { + colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>"; + } else { + colorEl = ''; + } + if (label.id) { + selectedClass.push('label-item'); + $a.attr('data-label-id', label.id); + } + $a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title); + return $li.html($a).prop('outerHTML'); + }, + persistWhenHide: $dropdown.data('persistWhenHide'), + search: { + fields: ['title'] + }, + selectable: true, + filterable: true, + toggleLabel: function(selected, el) { + var selected_labels; + selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active'); + if (selected && (selected.title != null)) { + if (selected_labels.length > 1) { + return selected.title + " +" + (selected_labels.length - 1) + " more"; + } else { + return selected.title; + } + } else if (!selected && selected_labels.length !== 0) { + if (selected_labels.length > 1) { + return ($(selected_labels[0]).text()) + " +" + (selected_labels.length - 1) + " more"; + } else if (selected_labels.length === 1) { + return $(selected_labels).text(); + } + } else { + return defaultLabel; + } + }, + fieldName: $dropdown.data('field-name'), + id: function(label) { + if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) { + return label.title; + } else { + return label.id; + } + }, + hidden: function() { + var isIssueIndex, isMRIndex, page, selectedLabels; + page = $('body').data('page'); + isIssueIndex = page === 'projects:issues:index'; + isMRIndex = page === 'projects:merge_requests:index'; + $selectbox.hide(); + $value.removeAttr('style'); + if ($dropdown.hasClass('js-multiselect')) { + if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { + selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']"); + Issuable.filterResults($dropdown.closest('form')); + } else if ($dropdown.hasClass('js-filter-submit')) { + $dropdown.closest('form').submit(); + } else { + if (!$dropdown.hasClass('js-filter-bulk-update')) { + saveLabelData(); + } + } + } + if ($dropdown.hasClass('js-filter-bulk-update')) { + if (!this.options.persistWhenHide) { + return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass(); + } + } + }, + multiSelect: $dropdown.hasClass('js-multiselect'), + clicked: function(label) { + var isIssueIndex, isMRIndex, page; + _this.enableBulkLabelDropdown(); + if ($dropdown.hasClass('js-filter-bulk-update')) { + return; + } + page = $('body').data('page'); + isIssueIndex = page === 'projects:issues:index'; + isMRIndex = page === 'projects:merge_requests:index'; + if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { + if (!$dropdown.hasClass('js-multiselect')) { + selectedLabel = label.title; + return Issuable.filterResults($dropdown.closest('form')); + } + } else if ($dropdown.hasClass('js-filter-submit')) { + return $dropdown.closest('form').submit(); + } else { + if ($dropdown.hasClass('js-multiselect')) { + + } else { + return saveLabelData(); + } + } + }, + setIndeterminateIds: function() { + if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { + return this.indeterminateIds = _this.getIndeterminateIds(); + } + }, + setActiveIds: function() { + if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { + return this.activeIds = _this.getActiveIds(); + } + } + }); + }); + this.bindEvents(); + } + + LabelsSelect.prototype.bindEvents = function() { + return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue); + }; + + LabelsSelect.prototype.onSelectCheckboxIssue = function() { + if ($('.selected_issue:checked').length) { + return; + } + $('.issues_bulk_update .labels-filter input[type="hidden"]').remove(); + return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label'); + }; + + LabelsSelect.prototype.getIndeterminateIds = function() { + var label_ids; + label_ids = []; + $('.selected_issue:checked').each(function(i, el) { + var issue_id; + issue_id = $(el).data('id'); + return label_ids.push($("#issue_" + issue_id).data('labels')); + }); + return _.flatten(label_ids); + }; + + LabelsSelect.prototype.getActiveIds = function() { + var label_ids; + label_ids = []; + $('.selected_issue:checked').each(function(i, el) { + var issue_id; + issue_id = $(el).data('id'); + return label_ids.push($("#issue_" + issue_id).data('labels')); + }); + return _.intersection.apply(_, label_ids); + }; + + LabelsSelect.prototype.enableBulkLabelDropdown = function() { + var issuableBulkActions; + if ($('.selected_issue:checked').length) { + issuableBulkActions = $('.bulk-update').data('bulkActions'); + return issuableBulkActions.willUpdateLabels = true; + } + }; + + return LabelsSelect; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee deleted file mode 100644 index 7688609b301..00000000000 --- a/app/assets/javascripts/labels_select.js.coffee +++ /dev/null @@ -1,386 +0,0 @@ -class @LabelsSelect - constructor: -> - _this = @ - - $('.js-label-select').each (i, dropdown) -> - $dropdown = $(dropdown) - projectId = $dropdown.data('project-id') - labelUrl = $dropdown.data('labels') - issueUpdateURL = $dropdown.data('issueUpdate') - selectedLabel = $dropdown.data('selected') - if selectedLabel? and not $dropdown.hasClass 'js-multiselect' - selectedLabel = selectedLabel.split(',') - newLabelField = $('#new_label_name') - newColorField = $('#new_label_color') - showNo = $dropdown.data('show-no') - showAny = $dropdown.data('show-any') - defaultLabel = $dropdown.data('default-label') - abilityName = $dropdown.data('ability-name') - $selectbox = $dropdown.closest('.selectbox') - $block = $selectbox.closest('.block') - $form = $dropdown.closest('form') - $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span') - $value = $block.find('.value') - $newLabelError = $('.js-label-error') - $colorPreview = $('.js-dropdown-label-color-preview') - $newLabelCreateButton = $('.js-new-label-btn') - - $newLabelError.hide() - $loading = $block.find('.block-loading').fadeOut() - - issueURLSplit = issueUpdateURL.split('/') if issueUpdateURL? - if issueUpdateURL - labelHTMLTemplate = _.template( - '<% _.each(labels, function(label){ %> - <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> - <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> - <%- label.title %> - </span> - </a> - <% }); %>' - ) - labelNoneHTMLTemplate = '<span class="no-value">None</span>' - - if newLabelField.length - - # Suggested colors in the dropdown to chose from pre-chosen colors - $('.suggest-colors-dropdown a').on "click", (e) -> - e.preventDefault() - e.stopPropagation() - newColorField - .val($(this).data('color')) - .trigger('change') - $colorPreview - .css 'background-color', $(this).data('color') - .parent() - .addClass 'is-active' - - # Cancel button takes back to first page - resetForm = -> - newLabelField - .val '' - .trigger 'change' - newColorField - .val '' - .trigger 'change' - $colorPreview - .css 'background-color', '' - .parent() - .removeClass 'is-active' - - $('.dropdown-menu-back').on 'click', -> - resetForm() - - $('.js-cancel-label-btn').on 'click', (e) -> - e.preventDefault() - e.stopPropagation() - resetForm() - $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' - - # Listen for change and keyup events on label and color field - # This allows us to enable the button when ready - enableLabelCreateButton = -> - if newLabelField.val() isnt '' and newColorField.val() isnt '' - $newLabelError.hide() - $newLabelCreateButton.enable() - else - $newLabelCreateButton.disable() - - saveLabel = -> - # Create new label with API - Api.newLabel projectId, { - name: newLabelField.val() - color: newColorField.val() - }, (label) -> - $newLabelCreateButton.enable() - - if label.message? - errors = _.map label.message, (value, key) -> - "#{key} #{value[0]}" - - $newLabelError - .html errors.join("<br/>") - .show() - else - $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' - - newLabelField.on 'keyup change', enableLabelCreateButton - - newColorField.on 'keyup change', enableLabelCreateButton - - # Send the API call to create the label - $newLabelCreateButton - .disable() - .on 'click', (e) -> - e.preventDefault() - e.stopPropagation() - saveLabel() - - saveLabelData = -> - selected = $dropdown - .closest('.selectbox') - .find("input[name='#{$dropdown.data('field-name')}']") - .map(-> - @value - ).get() - data = {} - data[abilityName] = {} - data[abilityName].label_ids = selected - if not selected.length - data[abilityName].label_ids = [''] - $loading.fadeIn() - $dropdown.trigger('loading.gl.dropdown') - $.ajax( - type: 'PUT' - url: issueUpdateURL - dataType: 'JSON' - data: data - ).done (data) -> - $loading.fadeOut() - $dropdown.trigger('loaded.gl.dropdown') - $selectbox.hide() - data.issueURLSplit = issueURLSplit - labelCount = 0 - if data.labels.length - template = labelHTMLTemplate(data) - labelCount = data.labels.length - else - template = labelNoneHTMLTemplate - $value - .removeAttr('style') - .html(template) - $sidebarCollapsedValue.text(labelCount) - - $('.has-tooltip', $value).tooltip(container: 'body') - - $value - .find('a') - .each((i) -> - setTimeout(=> - gl.animate.animate($(@), 'pulse') - ,200 * i - ) - ) - - - $dropdown.glDropdown( - data: (term, callback) -> - $.ajax( - url: labelUrl - ).done (data) -> - data = _.chain data - .groupBy (label) -> - label.title - .map (label) -> - color = _.map label, (dup) -> - dup.color - - return { - id: label[0].id - title: label[0].title - color: color - duplicate: color.length > 1 - } - .value() - - if $dropdown.hasClass 'js-extra-options' - if showNo - data.unshift( - id: 0 - title: 'No Label' - ) - - if showAny - data.unshift( - isAny: true - title: 'Any Label' - ) - - if data.length > 2 - data.splice 2, 0, 'divider' - - callback data - - renderRow: (label, instance) -> - $li = $('<li>') - $a = $('<a href="#">') - - selectedClass = [] - removesAll = label.id is 0 or not label.id? - - if $dropdown.hasClass('js-filter-bulk-update') - indeterminate = instance.indeterminateIds - active = instance.activeIds - - if indeterminate.indexOf(label.id) isnt -1 - selectedClass.push 'is-indeterminate' - - if active.indexOf(label.id) isnt -1 - # Remove is-indeterminate class if the item will be marked as active - i = selectedClass.indexOf 'is-indeterminate' - selectedClass.splice i, 1 unless i is -1 - - selectedClass.push 'is-active' - - # Add input manually - instance.addInput @fieldName, label.id - - if $form.find("input[type='hidden']\ - [name='#{$dropdown.data('fieldName')}']\ - [value='#{this.id(label)}']").length - selectedClass.push 'is-active' - - if $dropdown.hasClass('js-multiselect') and removesAll - selectedClass.push 'dropdown-clear-active' - - if label.duplicate - spacing = 100 / label.color.length - - # Reduce the colors to 4 - label.color = label.color.filter (color, i) -> - i < 4 - - color = _.map(label.color, (color, i) -> - percentFirst = Math.floor(spacing * i) - percentSecond = Math.floor(spacing * (i + 1)) - "#{color} #{percentFirst}%,#{color} #{percentSecond}% " - ).join(',') - color = "linear-gradient(#{color})" - else - if label.color? - color = label.color[0] - - if color - colorEl = "<span class='dropdown-label-box' style='background: #{color}'></span>" - else - colorEl = '' - - # We need to identify which items are actually labels - if label.id - selectedClass.push('label-item') - $a.attr('data-label-id', label.id) - - $a.addClass(selectedClass.join(' ')) - .html("#{colorEl} #{label.title}") - - # Return generated html - $li.html($a).prop('outerHTML') - persistWhenHide: $dropdown.data('persistWhenHide') - search: - fields: ['title'] - selectable: true - filterable: true - toggleLabel: (selected, el) -> - selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active') - - if selected and selected.title? - if selected_labels.length > 1 - "#{selected.title} +#{selected_labels.length - 1} more" - else - selected.title - else if not selected and selected_labels.length isnt 0 - if selected_labels.length > 1 - "#{$(selected_labels[0]).text()} +#{selected_labels.length - 1} more" - else if selected_labels.length is 1 - $(selected_labels).text() - else - defaultLabel - fieldName: $dropdown.data('field-name') - id: (label) -> - if $dropdown.hasClass("js-filter-submit") and not label.isAny? - label.title - else - label.id - - hidden: -> - page = $('body').data 'page' - isIssueIndex = page is 'projects:issues:index' - isMRIndex = page is 'projects:merge_requests:index' - - $selectbox.hide() - # display:block overrides the hide-collapse rule - $value.removeAttr('style') - if $dropdown.hasClass 'js-multiselect' - if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) - selectedLabels = $dropdown - .closest('form') - .find("input:hidden[name='#{$dropdown.data('fieldName')}']") - Issuable.filterResults $dropdown.closest('form') - else if $dropdown.hasClass('js-filter-submit') - $dropdown.closest('form').submit() - else - if not $dropdown.hasClass 'js-filter-bulk-update' - saveLabelData() - - if $dropdown.hasClass('js-filter-bulk-update') - # If we are persisting state we need the classes - if not @options.persistWhenHide - $dropdown.parent().find('.is-active, .is-indeterminate').removeClass() - - multiSelect: $dropdown.hasClass 'js-multiselect' - clicked: (label) -> - _this.enableBulkLabelDropdown() - - if $dropdown.hasClass('js-filter-bulk-update') - return - - page = $('body').data 'page' - isIssueIndex = page is 'projects:issues:index' - isMRIndex = page is 'projects:merge_requests:index' - if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) - if not $dropdown.hasClass 'js-multiselect' - selectedLabel = label.title - Issuable.filterResults $dropdown.closest('form') - else if $dropdown.hasClass 'js-filter-submit' - $dropdown.closest('form').submit() - else - if $dropdown.hasClass 'js-multiselect' - return - else - saveLabelData() - - setIndeterminateIds: -> - if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') - @indeterminateIds = _this.getIndeterminateIds() - - setActiveIds: -> - if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') - @activeIds = _this.getActiveIds() - ) - - @bindEvents() - - bindEvents: -> - $('body').on 'change', '.selected_issue', @onSelectCheckboxIssue - - onSelectCheckboxIssue: -> - return if $('.selected_issue:checked').length - - # Remove inputs - $('.issues_bulk_update .labels-filter input[type="hidden"]').remove() - - # Also restore button text - $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label') - - getIndeterminateIds: -> - label_ids = [] - - $('.selected_issue:checked').each (i, el) -> - issue_id = $(el).data('id') - label_ids.push $("#issue_#{issue_id}").data('labels') - - _.flatten(label_ids) - - getActiveIds: -> - label_ids = [] - - $('.selected_issue:checked').each (i, el) -> - issue_id = $(el).data('id') - label_ids.push $("#issue_#{issue_id}").data('labels') - - _.intersection.apply _, label_ids - - enableBulkLabelDropdown: -> - if $('.selected_issue:checked').length - issuableBulkActions = $('.bulk-update').data('bulkActions') - issuableBulkActions.willUpdateLabels = true diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js new file mode 100644 index 00000000000..ce472f3bcd0 --- /dev/null +++ b/app/assets/javascripts/layout_nav.js @@ -0,0 +1,27 @@ +(function() { + var hideEndFade; + + hideEndFade = function($scrollingTabs) { + return $scrollingTabs.each(function() { + var $this; + $this = $(this); + return $this.siblings('.fade-right').toggleClass('scrolling', $this.width() < $this.prop('scrollWidth')); + }); + }; + + $(function() { + hideEndFade($('.scrolling-tabs')); + $(window).off('resize.nav').on('resize.nav', function() { + return hideEndFade($('.scrolling-tabs')); + }); + return $('.scrolling-tabs').on('scroll', function(event) { + var $this, currentPosition, maxPosition; + $this = $(this); + currentPosition = $this.scrollLeft(); + maxPosition = $this.prop('scrollWidth') - $this.outerWidth(); + $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0); + return $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1); + }); + }); + +}).call(this); diff --git a/app/assets/javascripts/layout_nav.js.coffee b/app/assets/javascripts/layout_nav.js.coffee deleted file mode 100644 index f639f7f5892..00000000000 --- a/app/assets/javascripts/layout_nav.js.coffee +++ /dev/null @@ -1,24 +0,0 @@ -hideEndFade = ($scrollingTabs) -> - $scrollingTabs.each -> - $this = $(@) - - $this - .siblings('.fade-right') - .toggleClass('scrolling', $this.width() < $this.prop('scrollWidth')) - -$ -> - - hideEndFade($('.scrolling-tabs')) - - $(window) - .off 'resize.nav' - .on 'resize.nav', -> - hideEndFade($('.scrolling-tabs')) - - $('.scrolling-tabs').on 'scroll', (event) -> - $this = $(this) - currentPosition = $this.scrollLeft() - maxPosition = $this.prop('scrollWidth') - $this.outerWidth() - - $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0) - $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1) diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js new file mode 100644 index 00000000000..8d5e52286b7 --- /dev/null +++ b/app/assets/javascripts/lib/chart.js @@ -0,0 +1,7 @@ + +/*= require Chart */ + +(function() { + + +}).call(this); diff --git a/app/assets/javascripts/lib/chart.js.coffee b/app/assets/javascripts/lib/chart.js.coffee deleted file mode 100644 index 82217fc5107..00000000000 --- a/app/assets/javascripts/lib/chart.js.coffee +++ /dev/null @@ -1 +0,0 @@ -#= require Chart diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js new file mode 100644 index 00000000000..8ee81804513 --- /dev/null +++ b/app/assets/javascripts/lib/cropper.js @@ -0,0 +1,7 @@ + +/*= require cropper */ + +(function() { + + +}).call(this); diff --git a/app/assets/javascripts/lib/cropper.js.coffee b/app/assets/javascripts/lib/cropper.js.coffee deleted file mode 100644 index 32536d23fe3..00000000000 --- a/app/assets/javascripts/lib/cropper.js.coffee +++ /dev/null @@ -1 +0,0 @@ -#= require cropper diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js new file mode 100644 index 00000000000..31e6033e756 --- /dev/null +++ b/app/assets/javascripts/lib/d3.js @@ -0,0 +1,7 @@ + +/*= require d3 */ + +(function() { + + +}).call(this); diff --git a/app/assets/javascripts/lib/d3.js.coffee b/app/assets/javascripts/lib/d3.js.coffee deleted file mode 100644 index 74f0a0bb06a..00000000000 --- a/app/assets/javascripts/lib/d3.js.coffee +++ /dev/null @@ -1 +0,0 @@ -#= require d3 diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js new file mode 100644 index 00000000000..923c575dcfe --- /dev/null +++ b/app/assets/javascripts/lib/raphael.js @@ -0,0 +1,13 @@ + +/*= require raphael */ + + +/*= require g.raphael */ + + +/*= require g.bar */ + +(function() { + + +}).call(this); diff --git a/app/assets/javascripts/lib/raphael.js.coffee b/app/assets/javascripts/lib/raphael.js.coffee deleted file mode 100644 index ab8e5979b87..00000000000 --- a/app/assets/javascripts/lib/raphael.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -#= require raphael -#= require g.raphael -#= require g.bar diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js new file mode 100644 index 00000000000..d36efdabc93 --- /dev/null +++ b/app/assets/javascripts/lib/utils/animate.js @@ -0,0 +1,49 @@ +(function() { + (function(w) { + if (w.gl == null) { + w.gl = {}; + } + if (gl.animate == null) { + gl.animate = {}; + } + gl.animate.animate = function($el, animation, options, done) { + if ((options != null ? options.cssStart : void 0) != null) { + $el.css(options.cssStart); + } + $el.removeClass(animation + ' animated').addClass(animation + ' animated').one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function() { + $(this).removeClass(animation + ' animated'); + if (done != null) { + done(); + } + if ((options != null ? options.cssEnd : void 0) != null) { + $el.css(options.cssEnd); + } + }); + }; + gl.animate.animateEach = function($els, animation, time, options, done) { + var dfd; + dfd = $.Deferred(); + if (!$els.length) { + dfd.resolve(); + } + $els.each(function(i) { + setTimeout((function(_this) { + return function() { + var $this; + $this = $(_this); + return gl.animate.animate($this, animation, options, function() { + if (i === $els.length - 1) { + dfd.resolve(); + if (done != null) { + return done(); + } + } + }); + }; + })(this), time * i); + }); + return dfd.promise(); + }; + })(window); + +}).call(this); diff --git a/app/assets/javascripts/lib/utils/animate.js.coffee b/app/assets/javascripts/lib/utils/animate.js.coffee deleted file mode 100644 index ec3b44d6126..00000000000 --- a/app/assets/javascripts/lib/utils/animate.js.coffee +++ /dev/null @@ -1,39 +0,0 @@ -((w) -> - if not w.gl? then w.gl = {} - if not gl.animate? then gl.animate = {} - - gl.animate.animate = ($el, animation, options, done) -> - if options?.cssStart? - $el.css(options.cssStart) - $el - .removeClass(animation + ' animated') - .addClass(animation + ' animated') - .one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', -> - $(this).removeClass(animation + ' animated') - if done? - done() - if options?.cssEnd? - $el.css(options.cssEnd) - return - return - - gl.animate.animateEach = ($els, animation, time, options, done) -> - dfd = $.Deferred() - if not $els.length - dfd.resolve() - $els.each((i) -> - setTimeout(=> - $this = $(@) - gl.animate.animate($this, animation, options, => - if i is $els.length - 1 - dfd.resolve() - if done? - done() - ) - ,time * i - ) - return - ) - return dfd.promise() - return -) window
\ No newline at end of file diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js new file mode 100644 index 00000000000..9299d0eabd2 --- /dev/null +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -0,0 +1,60 @@ +(function() { + (function(w) { + var base; + w.gl || (w.gl = {}); + (base = w.gl).utils || (base.utils = {}); + w.gl.utils.isInGroupsPage = function() { + return gl.utils.getPagePath() === 'groups'; + }; + w.gl.utils.isInProjectPage = function() { + return gl.utils.getPagePath() === 'projects'; + }; + w.gl.utils.getProjectSlug = function() { + if (this.isInProjectPage()) { + return $('body').data('project'); + } else { + return null; + } + }; + w.gl.utils.getGroupSlug = function() { + if (this.isInGroupsPage()) { + return $('body').data('group'); + } else { + return null; + } + }; + gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) { + return $tooltipEl.tooltip('destroy').attr('title', newTitle).tooltip('fixTitle'); + }; + gl.utils.preventDisabledButtons = function() { + return $('.btn').click(function(e) { + if ($(this).hasClass('disabled')) { + e.preventDefault(); + e.stopImmediatePropagation(); + return false; + } + }); + }; + gl.utils.getPagePath = function() { + return $('body').data('page').split(':')[0]; + }; + return jQuery.timefor = function(time, suffix, expiredLabel) { + var suffixFromNow, timefor; + if (!time) { + return ''; + } + suffix || (suffix = 'remaining'); + expiredLabel || (expiredLabel = 'Past due'); + jQuery.timeago.settings.allowFuture = true; + suffixFromNow = jQuery.timeago.settings.strings.suffixFromNow; + jQuery.timeago.settings.strings.suffixFromNow = suffix; + timefor = $.timeago(time); + if (timefor.indexOf('ago') > -1) { + timefor = expiredLabel; + } + jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow; + return timefor; + }; + })(window); + +}).call(this); diff --git a/app/assets/javascripts/lib/utils/common_utils.js.coffee b/app/assets/javascripts/lib/utils/common_utils.js.coffee deleted file mode 100644 index d4dd3dc329a..00000000000 --- a/app/assets/javascripts/lib/utils/common_utils.js.coffee +++ /dev/null @@ -1,68 +0,0 @@ -((w) -> - - w.gl or= {} - w.gl.utils or= {} - - w.gl.utils.isInGroupsPage = -> - - return gl.utils.getPagePath() is 'groups' - - - w.gl.utils.isInProjectPage = -> - - return gl.utils.getPagePath() is 'projects' - - - w.gl.utils.getProjectSlug = -> - - return if @isInProjectPage() then $('body').data 'project' else null - - - w.gl.utils.getGroupSlug = -> - - return if @isInGroupsPage() then $('body').data 'group' else null - - - - gl.utils.updateTooltipTitle = ($tooltipEl, newTitle) -> - - $tooltipEl - .tooltip 'destroy' - .attr 'title', newTitle - .tooltip 'fixTitle' - - - gl.utils.preventDisabledButtons = -> - - $('.btn').click (e) -> - if $(this).hasClass 'disabled' - e.preventDefault() - e.stopImmediatePropagation() - return false - - gl.utils.getPagePath = -> - return $('body').data('page').split(':')[0] - - - jQuery.timefor = (time, suffix, expiredLabel) -> - - return '' unless time - - suffix or= 'remaining' - expiredLabel or= 'Past due' - - jQuery.timeago.settings.allowFuture = yes - - { suffixFromNow } = jQuery.timeago.settings.strings - jQuery.timeago.settings.strings.suffixFromNow = suffix - - timefor = $.timeago time - - if timefor.indexOf('ago') > -1 - timefor = expiredLabel - - jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow - - return timefor - -) window diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js new file mode 100644 index 00000000000..e817261f210 --- /dev/null +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -0,0 +1,36 @@ +(function() { + (function(w) { + var base; + if (w.gl == null) { + w.gl = {}; + } + if ((base = w.gl).utils == null) { + base.utils = {}; + } + w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + w.gl.utils.formatDate = function(datetime) { + return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z'); + }; + w.gl.utils.getDayName = function(date) { + return this.days[date.getDay()]; + }; + return w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago) { + if (setTimeago == null) { + setTimeago = true; + } + $timeagoEls.each(function() { + var $el; + $el = $(this); + return $el.attr('title', gl.utils.formatDate($el.attr('datetime'))); + }); + if (setTimeago) { + $timeagoEls.timeago(); + $timeagoEls.tooltip('destroy'); + return $timeagoEls.tooltip({ + template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' + }); + } + }; + })(window); + +}).call(this); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js.coffee b/app/assets/javascripts/lib/utils/datetime_utility.js.coffee deleted file mode 100644 index 2371e913844..00000000000 --- a/app/assets/javascripts/lib/utils/datetime_utility.js.coffee +++ /dev/null @@ -1,28 +0,0 @@ -((w) -> - - w.gl ?= {} - w.gl.utils ?= {} - w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] - - w.gl.utils.formatDate = (datetime) -> - dateFormat(datetime, 'mmm d, yyyy h:MMtt Z') - - w.gl.utils.getDayName = (date) -> - this.days[date.getDay()] - - w.gl.utils.localTimeAgo = ($timeagoEls, setTimeago = true) -> - $timeagoEls.each( -> - $el = $(@) - $el.attr('title', gl.utils.formatDate($el.attr('datetime'))) - ) - - if setTimeago - $timeagoEls.timeago() - $timeagoEls.tooltip('destroy') - - # Recreate with custom template - $timeagoEls.tooltip( - template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' - ) - -) window diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js new file mode 100644 index 00000000000..42b6ac0589e --- /dev/null +++ b/app/assets/javascripts/lib/utils/notify.js @@ -0,0 +1,41 @@ +(function() { + (function(w) { + var notificationGranted, notifyMe, notifyPermissions; + notificationGranted = function(message, opts, onclick) { + var notification; + notification = new Notification(message, opts); + setTimeout(function() { + return notification.close(); + }, 8000); + if (onclick) { + return notification.onclick = onclick; + } + }; + notifyPermissions = function() { + if ('Notification' in window) { + return Notification.requestPermission(); + } + }; + notifyMe = function(message, body, icon, onclick) { + var opts; + opts = { + body: body, + icon: icon + }; + if (!('Notification' in window)) { + + } else if (Notification.permission === 'granted') { + return notificationGranted(message, opts, onclick); + } else if (Notification.permission !== 'denied') { + return Notification.requestPermission(function(permission) { + if (permission === 'granted') { + return notificationGranted(message, opts, onclick); + } + }); + } + }; + w.notify = notifyMe; + return w.notifyPermissions = notifyPermissions; + })(window); + +}).call(this); diff --git a/app/assets/javascripts/lib/utils/notify.js.coffee b/app/assets/javascripts/lib/utils/notify.js.coffee deleted file mode 100644 index 9e28353ac34..00000000000 --- a/app/assets/javascripts/lib/utils/notify.js.coffee +++ /dev/null @@ -1,35 +0,0 @@ -((w) -> - notificationGranted = (message, opts, onclick) -> - notification = new Notification(message, opts) - - # Hide the notification after X amount of seconds - setTimeout -> - notification.close() - , 8000 - - if onclick - notification.onclick = onclick - - notifyPermissions = -> - if 'Notification' of window - Notification.requestPermission() - - notifyMe = (message, body, icon, onclick) -> - opts = - body: body - icon: icon - # Let's check if the browser supports notifications - if !('Notification' of window) - # do nothing - else if Notification.permission == 'granted' - # If it's okay let's create a notification - notificationGranted message, opts, onclick - else if Notification.permission != 'denied' - Notification.requestPermission (permission) -> - # If the user accepts, let's create a notification - if permission == 'granted' - notificationGranted message, opts, onclick - - w.notify = notifyMe - w.notifyPermissions = notifyPermissions -) window diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js new file mode 100644 index 00000000000..130479642f3 --- /dev/null +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -0,0 +1,112 @@ +(function() { + (function(w) { + var base; + if (w.gl == null) { + w.gl = {}; + } + if ((base = w.gl).text == null) { + base.text = {}; + } + gl.text.randomString = function() { + return Math.random().toString(36).substring(7); + }; + gl.text.replaceRange = function(s, start, end, substitute) { + return s.substring(0, start) + substitute + s.substring(end); + }; + gl.text.selectedText = function(text, textarea) { + return text.substring(textarea.selectionStart, textarea.selectionEnd); + }; + gl.text.lineBefore = function(text, textarea) { + var split; + split = text.substring(0, textarea.selectionStart).trim().split('\n'); + return split[split.length - 1]; + }; + gl.text.lineAfter = function(text, textarea) { + return text.substring(textarea.selectionEnd).trim().split('\n')[0]; + }; + gl.text.blockTagText = function(text, textArea, blockTag, selected) { + var lineAfter, lineBefore; + lineBefore = this.lineBefore(text, textArea); + lineAfter = this.lineAfter(text, textArea); + if (lineBefore === blockTag && lineAfter === blockTag) { + if (blockTag != null) { + textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1); + textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1); + } + return selected; + } else { + return blockTag + "\n" + selected + "\n" + blockTag; + } + }; + gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) { + var insertText, inserted, selectedSplit, startChar; + selectedSplit = selected.split('\n'); + startChar = !wrap && textArea.selectionStart > 0 ? '\n' : ''; + if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) { + if (blockTag != null) { + insertText = this.blockTagText(text, textArea, blockTag, selected); + } else { + insertText = selectedSplit.map(function(val) { + if (val.indexOf(tag) === 0) { + return "" + (val.replace(tag, '')); + } else { + return "" + tag + val; + } + }).join('\n'); + } + } else { + insertText = "" + startChar + tag + selected + (wrap ? tag : ' '); + } + if (document.queryCommandSupported('insertText')) { + inserted = document.execCommand('insertText', false, insertText); + } + if (!inserted) { + try { + document.execCommand("ms-beginUndoUnit"); + } catch (undefined) {} + textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText); + try { + document.execCommand("ms-endUndoUnit"); + } catch (undefined) {} + } + return this.moveCursor(textArea, tag, wrap); + }; + gl.text.moveCursor = function(textArea, tag, wrapped) { + var pos; + if (!textArea.setSelectionRange) { + return; + } + if (textArea.selectionStart === textArea.selectionEnd) { + if (wrapped) { + pos = textArea.selectionStart - tag.length; + } else { + pos = textArea.selectionStart; + } + return textArea.setSelectionRange(pos, pos); + } + }; + gl.text.updateText = function(textArea, tag, blockTag, wrap) { + var $textArea, oldVal, selected, text; + $textArea = $(textArea); + oldVal = $textArea.val(); + textArea = $textArea.get(0); + text = $textArea.val(); + selected = this.selectedText(text, textArea); + $textArea.focus(); + return this.insertText(textArea, text, tag, blockTag, selected, wrap); + }; + gl.text.init = function(form) { + var self; + self = this; + return $('.js-md', form).off('click').on('click', function() { + var $this; + $this = $(this); + return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend')); + }); + }; + return gl.text.removeListeners = function(form) { + return $('.js-md', form).off(); + }; + })(window); + +}).call(this); diff --git a/app/assets/javascripts/lib/utils/text_utility.js.coffee b/app/assets/javascripts/lib/utils/text_utility.js.coffee deleted file mode 100644 index 2e1407f8738..00000000000 --- a/app/assets/javascripts/lib/utils/text_utility.js.coffee +++ /dev/null @@ -1,105 +0,0 @@ -((w) -> - w.gl ?= {} - w.gl.text ?= {} - - gl.text.randomString = -> Math.random().toString(36).substring(7) - - gl.text.replaceRange = (s, start, end, substitute) -> - s.substring(0, start) + substitute + s.substring(end); - - gl.text.selectedText = (text, textarea) -> - text.substring(textarea.selectionStart, textarea.selectionEnd) - - gl.text.lineBefore = (text, textarea) -> - split = text.substring(0, textarea.selectionStart).trim().split('\n') - split[split.length - 1] - - gl.text.lineAfter = (text, textarea) -> - text.substring(textarea.selectionEnd).trim().split('\n')[0] - - gl.text.blockTagText = (text, textArea, blockTag, selected) -> - lineBefore = @lineBefore(text, textArea) - lineAfter = @lineAfter(text, textArea) - - if lineBefore is blockTag and lineAfter is blockTag - # To remove the block tag we have to select the line before & after - if blockTag? - textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1) - textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1) - - selected - else - "#{blockTag}\n#{selected}\n#{blockTag}" - - gl.text.insertText = (textArea, text, tag, blockTag, selected, wrap) -> - selectedSplit = selected.split('\n') - startChar = if not wrap and textArea.selectionStart > 0 then '\n' else '' - - if selectedSplit.length > 1 and (not wrap or blockTag?) - if blockTag? - insertText = @blockTagText(text, textArea, blockTag, selected) - else - insertText = selectedSplit.map((val) -> - if val.indexOf(tag) is 0 - "#{val.replace(tag, '')}" - else - "#{tag}#{val}" - ).join('\n') - else - insertText = "#{startChar}#{tag}#{selected}#{if wrap then tag else ' '}" - - if document.queryCommandSupported('insertText') - inserted = document.execCommand 'insertText', false, insertText - - unless inserted - try - document.execCommand("ms-beginUndoUnit") - - textArea.value = @replaceRange( - text, - textArea.selectionStart, - textArea.selectionEnd, - insertText) - try - document.execCommand("ms-endUndoUnit") - - @moveCursor(textArea, tag, wrap) - - gl.text.moveCursor = (textArea, tag, wrapped) -> - return unless textArea.setSelectionRange - - if textArea.selectionStart is textArea.selectionEnd - if wrapped - pos = textArea.selectionStart - tag.length - else - pos = textArea.selectionStart - - textArea.setSelectionRange pos, pos - - gl.text.updateText = (textArea, tag, blockTag, wrap) -> - $textArea = $(textArea) - oldVal = $textArea.val() - textArea = $textArea.get(0) - text = $textArea.val() - selected = @selectedText(text, textArea) - $textArea.focus() - - @insertText(textArea, text, tag, blockTag, selected, wrap) - - gl.text.init = (form) -> - self = @ - $('.js-md', form) - .off 'click' - .on 'click', -> - $this = $(@) - self.updateText( - $this.closest('.md-area').find('textarea'), - $this.data('md-tag'), - $this.data('md-block'), - not $this.data('md-prepend') - ) - - gl.text.removeListeners = (form) -> - $('.js-md', form).off() - -) window diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js new file mode 100644 index 00000000000..dc30babd645 --- /dev/null +++ b/app/assets/javascripts/lib/utils/type_utility.js @@ -0,0 +1,15 @@ +(function() { + (function(w) { + var base; + if (w.gl == null) { + w.gl = {}; + } + if ((base = w.gl).utils == null) { + base.utils = {}; + } + return w.gl.utils.isObject = function(obj) { + return (obj != null) && (obj.constructor === Object); + }; + })(window); + +}).call(this); diff --git a/app/assets/javascripts/lib/utils/type_utility.js.coffee b/app/assets/javascripts/lib/utils/type_utility.js.coffee deleted file mode 100644 index 957f0d86b36..00000000000 --- a/app/assets/javascripts/lib/utils/type_utility.js.coffee +++ /dev/null @@ -1,9 +0,0 @@ -((w) -> - - w.gl ?= {} - w.gl.utils ?= {} - - w.gl.utils.isObject = (obj) -> - obj? and (obj.constructor is Object) - -) window diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js new file mode 100644 index 00000000000..fffbfd19745 --- /dev/null +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -0,0 +1,64 @@ +(function() { + (function(w) { + var base; + if (w.gl == null) { + w.gl = {}; + } + if ((base = w.gl).utils == null) { + base.utils = {}; + } + w.gl.utils.getParameterValues = function(sParam) { + var i, sPageURL, sParameterName, sURLVariables, values; + sPageURL = decodeURIComponent(window.location.search.substring(1)); + sURLVariables = sPageURL.split('&'); + sParameterName = void 0; + values = []; + i = 0; + while (i < sURLVariables.length) { + sParameterName = sURLVariables[i].split('='); + if (sParameterName[0] === sParam) { + values.push(sParameterName[1]); + } + i++; + } + return values; + }; + w.gl.utils.mergeUrlParams = function(params, url) { + var lastChar, newUrl, paramName, paramValue, pattern; + newUrl = decodeURIComponent(url); + for (paramName in params) { + paramValue = params[paramName]; + pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)"); + if (paramValue == null) { + newUrl = newUrl.replace(pattern, ''); + } else if (url.search(pattern) !== -1) { + newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2"); + } else { + newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue; + } + } + lastChar = newUrl[newUrl.length - 1]; + if (lastChar === '&') { + newUrl = newUrl.slice(0, -1); + } + return newUrl; + }; + return w.gl.utils.removeParamQueryString = function(url, param) { + var urlVariables, variables; + url = decodeURIComponent(url); + urlVariables = url.split('&'); + return ((function() { + var j, len, results; + results = []; + for (j = 0, len = urlVariables.length; j < len; j++) { + variables = urlVariables[j]; + if (variables.indexOf(param) === -1) { + results.push(variables); + } + } + return results; + })()).join('&'); + }; + })(window); + +}).call(this); diff --git a/app/assets/javascripts/lib/utils/url_utility.js.coffee b/app/assets/javascripts/lib/utils/url_utility.js.coffee deleted file mode 100644 index e8085e1c2e4..00000000000 --- a/app/assets/javascripts/lib/utils/url_utility.js.coffee +++ /dev/null @@ -1,52 +0,0 @@ -((w) -> - - w.gl ?= {} - w.gl.utils ?= {} - - # Returns an array containing the value(s) of the - # of the key passed as an argument - w.gl.utils.getParameterValues = (sParam) -> - sPageURL = decodeURIComponent(window.location.search.substring(1)) - sURLVariables = sPageURL.split('&') - sParameterName = undefined - values = [] - i = 0 - while i < sURLVariables.length - sParameterName = sURLVariables[i].split('=') - if sParameterName[0] is sParam - values.push(sParameterName[1]) - i++ - values - - # # - # @param {Object} params - url keys and value to merge - # @param {String} url - # # - w.gl.utils.mergeUrlParams = (params, url) -> - newUrl = decodeURIComponent(url) - for paramName, paramValue of params - pattern = new RegExp "\\b(#{paramName}=).*?(&|$)" - if not paramValue? - newUrl = newUrl.replace pattern, '' - else if url.search(pattern) isnt -1 - newUrl = newUrl.replace pattern, "$1#{paramValue}$2" - else - newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}" - - # Remove a trailing ampersand - lastChar = newUrl[newUrl.length - 1] - - if lastChar is '&' - newUrl = newUrl.slice 0, -1 - - newUrl - - # removes parameter query string from url. returns the modified url - w.gl.utils.removeParamQueryString = (url, param) -> - url = decodeURIComponent(url) - urlVariables = url.split('&') - ( - variables for variables in urlVariables when variables.indexOf(param) is -1 - ).join('&') - -) window diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js new file mode 100644 index 00000000000..f145bd3ad74 --- /dev/null +++ b/app/assets/javascripts/line_highlighter.js @@ -0,0 +1,115 @@ + +/*= require jquery.scrollTo */ + +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.LineHighlighter = (function() { + LineHighlighter.prototype.highlightClass = 'hll'; + + LineHighlighter.prototype._hash = ''; + + function LineHighlighter(hash) { + var range; + if (hash == null) { + hash = location.hash; + } + this.setHash = bind(this.setHash, this); + this.highlightLine = bind(this.highlightLine, this); + this.clickHandler = bind(this.clickHandler, this); + this._hash = hash; + this.bindEvents(); + if (hash !== '') { + range = this.hashToRange(hash); + if (range[0]) { + this.highlightRange(range); + $.scrollTo("#L" + range[0], { + offset: -150 + }); + } + } + } + + LineHighlighter.prototype.bindEvents = function() { + $('#blob-content-holder').on('mousedown', 'a[data-line-number]', this.clickHandler); + return $('#blob-content-holder').on('click', 'a[data-line-number]', function(event) { + return event.preventDefault(); + }); + }; + + LineHighlighter.prototype.clickHandler = function(event) { + var current, lineNumber, range; + event.preventDefault(); + this.clearHighlight(); + lineNumber = $(event.target).closest('a').data('line-number'); + current = this.hashToRange(this._hash); + if (!(current[0] && event.shiftKey)) { + this.setHash(lineNumber); + return this.highlightLine(lineNumber); + } else if (event.shiftKey) { + if (lineNumber < current[0]) { + range = [lineNumber, current[0]]; + } else { + range = [current[0], lineNumber]; + } + this.setHash(range[0], range[1]); + return this.highlightRange(range); + } + }; + + LineHighlighter.prototype.clearHighlight = function() { + return $("." + this.highlightClass).removeClass(this.highlightClass); + }; + + LineHighlighter.prototype.hashToRange = function(hash) { + var first, last, matches; + matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/); + if (matches && matches.length) { + first = parseInt(matches[1]); + last = matches[2] ? parseInt(matches[2]) : null; + return [first, last]; + } else { + return [null, null]; + } + }; + + LineHighlighter.prototype.highlightLine = function(lineNumber) { + return $("#LC" + lineNumber).addClass(this.highlightClass); + }; + + LineHighlighter.prototype.highlightRange = function(range) { + var i, lineNumber, ref, ref1, results; + if (range[1]) { + results = []; + for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? ++i : --i) { + results.push(this.highlightLine(lineNumber)); + } + return results; + } else { + return this.highlightLine(range[0]); + } + }; + + LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) { + var hash; + if (lastLineNumber) { + hash = "#L" + firstLineNumber + "-" + lastLineNumber; + } else { + hash = "#L" + firstLineNumber; + } + this._hash = hash; + return this.__setLocationHash__(hash); + }; + + LineHighlighter.prototype.__setLocationHash__ = function(value) { + return history.pushState({ + turbolinks: false, + url: value + }, document.title, value); + }; + + return LineHighlighter; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/line_highlighter.js.coffee b/app/assets/javascripts/line_highlighter.js.coffee deleted file mode 100644 index 2254a3f91ae..00000000000 --- a/app/assets/javascripts/line_highlighter.js.coffee +++ /dev/null @@ -1,148 +0,0 @@ -# LineHighlighter -# -# Handles single- and multi-line selection and highlight for blob views. -# -#= require jquery.scrollTo -# -# ### Example Markup -# -# <div id="blob-content-holder"> -# <div class="file-content"> -# <div class="line-numbers"> -# <a href="#L1" id="L1" data-line-number="1">1</a> -# <a href="#L2" id="L2" data-line-number="2">2</a> -# <a href="#L3" id="L3" data-line-number="3">3</a> -# <a href="#L4" id="L4" data-line-number="4">4</a> -# <a href="#L5" id="L5" data-line-number="5">5</a> -# </div> -# <pre class="code highlight"> -# <code> -# <span id="LC1" class="line">...</span> -# <span id="LC2" class="line">...</span> -# <span id="LC3" class="line">...</span> -# <span id="LC4" class="line">...</span> -# <span id="LC5" class="line">...</span> -# </code> -# </pre> -# </div> -# </div> -# -class @LineHighlighter - # CSS class applied to highlighted lines - highlightClass: 'hll' - - # Internal copy of location.hash so we're not dependent on `location` in tests - _hash: '' - - # Initialize a LineHighlighter object - # - # hash - String URL hash for dependency injection in tests - constructor: (hash = location.hash) -> - @_hash = hash - - @bindEvents() - - unless hash == '' - range = @hashToRange(hash) - - if range[0] - @highlightRange(range) - - # Scroll to the first highlighted line on initial load - # Offset -50 for the sticky top bar, and another -100 for some context - $.scrollTo("#L#{range[0]}", offset: -150) - - bindEvents: -> - $('#blob-content-holder').on 'mousedown', 'a[data-line-number]', @clickHandler - - # While it may seem odd to bind to the mousedown event and then throw away - # the click event, there is a method to our madness. - # - # If not done this way, the line number anchor will sometimes keep its - # active state even when the event is cancelled, resulting in an ugly border - # around the link and/or a persisted underline text decoration. - - $('#blob-content-holder').on 'click', 'a[data-line-number]', (event) -> - event.preventDefault() - - clickHandler: (event) => - event.preventDefault() - - @clearHighlight() - - lineNumber = $(event.target).closest('a').data('line-number') - current = @hashToRange(@_hash) - - unless current[0] && event.shiftKey - # If there's no current selection, or there is but Shift wasn't held, - # treat this like a single-line selection. - @setHash(lineNumber) - @highlightLine(lineNumber) - else if event.shiftKey - if lineNumber < current[0] - range = [lineNumber, current[0]] - else - range = [current[0], lineNumber] - - @setHash(range[0], range[1]) - @highlightRange(range) - - # Unhighlight previously highlighted lines - clearHighlight: -> - $(".#{@highlightClass}").removeClass(@highlightClass) - - # Convert a URL hash String into line numbers - # - # hash - Hash String - # - # Examples: - # - # hashToRange('#L5') # => [5, null] - # hashToRange('#L5-15') # => [5, 15] - # hashToRange('#foo') # => [null, null] - # - # Returns an Array - hashToRange: (hash) -> - matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/) - - if matches && matches.length - first = parseInt(matches[1]) - last = if matches[2] then parseInt(matches[2]) else null - - [first, last] - else - [null, null] - - # Highlight a single line - # - # lineNumber - Line number to highlight - highlightLine: (lineNumber) => - $("#LC#{lineNumber}").addClass(@highlightClass) - - # Highlight all lines within a range - # - # range - Array containing the starting and ending line numbers - highlightRange: (range) -> - if range[1] - for lineNumber in [range[0]..range[1]] - @highlightLine(lineNumber) - else - @highlightLine(range[0]) - - # Set the URL hash string - setHash: (firstLineNumber, lastLineNumber) => - if lastLineNumber - hash = "#L#{firstLineNumber}-#{lastLineNumber}" - else - hash = "#L#{firstLineNumber}" - - @_hash = hash - @__setLocationHash__(hash) - - # Make the actual hash change in the browser - # - # This method is stubbed in tests. - __setLocationHash__: (value) -> - # We're using pushState instead of assigning location.hash directly to - # prevent the page from scrolling on the hashchange event - history.pushState({turbolinks: false, url: value}, document.title, value) diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js new file mode 100644 index 00000000000..218f24fe908 --- /dev/null +++ b/app/assets/javascripts/logo.js @@ -0,0 +1,54 @@ +(function() { + var clearHighlights, currentTimer, defaultClass, delay, firstPiece, pieceIndex, pieces, start, stop, work; + + Turbolinks.enableProgressBar(); + + defaultClass = 'tanuki-shape'; + + pieces = ['path#tanuki-right-cheek', 'path#tanuki-right-eye, path#tanuki-right-ear', 'path#tanuki-nose', 'path#tanuki-left-eye, path#tanuki-left-ear', 'path#tanuki-left-cheek']; + + pieceIndex = 0; + + firstPiece = pieces[0]; + + currentTimer = null; + + delay = 150; + + clearHighlights = function() { + return $("." + defaultClass + ".highlight").attr('class', defaultClass); + }; + + start = function() { + clearHighlights(); + pieceIndex = 0; + if (pieces[0] !== firstPiece) { + pieces.reverse(); + } + if (currentTimer) { + clearInterval(currentTimer); + } + return currentTimer = setInterval(work, delay); + }; + + stop = function() { + clearInterval(currentTimer); + return clearHighlights(); + }; + + work = function() { + clearHighlights(); + $(pieces[pieceIndex]).attr('class', defaultClass + " highlight"); + if (pieceIndex === pieces.length - 1) { + pieceIndex = 0; + return pieces.reverse(); + } else { + return pieceIndex++; + } + }; + + $(document).on('page:fetch', start); + + $(document).on('page:change', stop); + +}).call(this); diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee deleted file mode 100644 index dc2590a0355..00000000000 --- a/app/assets/javascripts/logo.js.coffee +++ /dev/null @@ -1,44 +0,0 @@ -Turbolinks.enableProgressBar(); - -defaultClass = 'tanuki-shape' -pieces = [ - 'path#tanuki-right-cheek', - 'path#tanuki-right-eye, path#tanuki-right-ear', - 'path#tanuki-nose', - 'path#tanuki-left-eye, path#tanuki-left-ear', - 'path#tanuki-left-cheek', -] -pieceIndex = 0 -firstPiece = pieces[0] - -currentTimer = null -delay = 150 - -clearHighlights = -> - $(".#{defaultClass}.highlight").attr('class', defaultClass) - -start = -> - clearHighlights() - pieceIndex = 0 - pieces.reverse() unless pieces[0] == firstPiece - clearInterval(currentTimer) if currentTimer - currentTimer = setInterval(work, delay) - -stop = -> - clearInterval(currentTimer) - clearHighlights() - -work = -> - clearHighlights() - $(pieces[pieceIndex]).attr('class', "#{defaultClass} highlight") - - # If we hit the last piece, reset the index and then reverse the array to - # get a nice back-and-forth sweeping look - if pieceIndex == pieces.length - 1 - pieceIndex = 0 - pieces.reverse() - else - pieceIndex++ - -$(document).on('page:fetch', start) -$(document).on('page:change', stop) diff --git a/app/assets/javascripts/markdown_preview.js b/app/assets/javascripts/markdown_preview.js new file mode 100644 index 00000000000..18fc7bae09a --- /dev/null +++ b/app/assets/javascripts/markdown_preview.js @@ -0,0 +1,150 @@ +(function() { + var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector; + + this.MarkdownPreview = (function() { + function MarkdownPreview() {} + + MarkdownPreview.prototype.referenceThreshold = 10; + + MarkdownPreview.prototype.ajaxCache = {}; + + MarkdownPreview.prototype.showPreview = function(form) { + var mdText, preview; + preview = form.find('.js-md-preview'); + mdText = form.find('textarea.markdown-area').val(); + if (mdText.trim().length === 0) { + preview.text('Nothing to preview.'); + return this.hideReferencedUsers(form); + } else { + preview.text('Loading...'); + return this.renderMarkdown(mdText, (function(_this) { + return function(response) { + preview.html(response.body); + preview.syntaxHighlight(); + return _this.renderReferencedUsers(response.references.users, form); + }; + })(this)); + } + }; + + MarkdownPreview.prototype.renderMarkdown = function(text, success) { + if (!window.markdown_preview_path) { + return; + } + if (text === this.ajaxCache.text) { + return success(this.ajaxCache.response); + } + return $.ajax({ + type: 'POST', + url: window.markdown_preview_path, + data: { + text: text + }, + dataType: 'json', + success: (function(_this) { + return function(response) { + _this.ajaxCache = { + text: text, + response: response + }; + return success(response); + }; + })(this) + }); + }; + + MarkdownPreview.prototype.hideReferencedUsers = function(form) { + var referencedUsers; + referencedUsers = form.find('.referenced-users'); + return referencedUsers.hide(); + }; + + MarkdownPreview.prototype.renderReferencedUsers = function(users, form) { + var referencedUsers; + referencedUsers = form.find('.referenced-users'); + if (referencedUsers.length) { + if (users.length >= this.referenceThreshold) { + referencedUsers.show(); + return referencedUsers.find('.js-referenced-users-count').text(users.length); + } else { + return referencedUsers.hide(); + } + } + }; + + return MarkdownPreview; + + })(); + + markdownPreview = new MarkdownPreview(); + + previewButtonSelector = '.js-md-preview-button'; + + writeButtonSelector = '.js-md-write-button'; + + lastTextareaPreviewed = null; + + $.fn.setupMarkdownPreview = function() { + var $form, form_textarea; + $form = $(this); + form_textarea = $form.find('textarea.markdown-area'); + form_textarea.on('input', function() { + return markdownPreview.hideReferencedUsers($form); + }); + return form_textarea.on('blur', function() { + return markdownPreview.showPreview($form); + }); + }; + + $(document).on('markdown-preview:show', function(e, $form) { + if (!$form) { + return; + } + lastTextareaPreviewed = $form.find('textarea.markdown-area'); + $form.find(writeButtonSelector).parent().removeClass('active'); + $form.find(previewButtonSelector).parent().addClass('active'); + $form.find('.md-write-holder').hide(); + $form.find('.md-preview-holder').show(); + return markdownPreview.showPreview($form); + }); + + $(document).on('markdown-preview:hide', function(e, $form) { + if (!$form) { + return; + } + lastTextareaPreviewed = null; + $form.find(writeButtonSelector).parent().addClass('active'); + $form.find(previewButtonSelector).parent().removeClass('active'); + $form.find('.md-write-holder').show(); + $form.find('textarea.markdown-area').focus(); + return $form.find('.md-preview-holder').hide(); + }); + + $(document).on('markdown-preview:toggle', function(e, keyboardEvent) { + var $target; + $target = $(keyboardEvent.target); + if ($target.is('textarea.markdown-area')) { + $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]); + return keyboardEvent.preventDefault(); + } else if (lastTextareaPreviewed) { + $target = lastTextareaPreviewed; + $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]); + return keyboardEvent.preventDefault(); + } + }); + + $(document).on('click', previewButtonSelector, function(e) { + var $form; + e.preventDefault(); + $form = $(this).closest('form'); + return $(document).triggerHandler('markdown-preview:show', [$form]); + }); + + $(document).on('click', writeButtonSelector, function(e) { + var $form; + e.preventDefault(); + $form = $(this).closest('form'); + return $(document).triggerHandler('markdown-preview:hide', [$form]); + }); + +}).call(this); diff --git a/app/assets/javascripts/markdown_preview.js.coffee b/app/assets/javascripts/markdown_preview.js.coffee deleted file mode 100644 index 2a0b9479445..00000000000 --- a/app/assets/javascripts/markdown_preview.js.coffee +++ /dev/null @@ -1,119 +0,0 @@ -# MarkdownPreview -# -# Handles toggling the "Write" and "Preview" tab clicks, rendering the preview, -# and showing a warning when more than `x` users are referenced. -# -class @MarkdownPreview - # Minimum number of users referenced before triggering a warning - referenceThreshold: 10 - ajaxCache: {} - - showPreview: (form) -> - preview = form.find('.js-md-preview') - mdText = form.find('textarea.markdown-area').val() - - if mdText.trim().length == 0 - preview.text('Nothing to preview.') - @hideReferencedUsers(form) - else - preview.text('Loading...') - @renderMarkdown mdText, (response) => - preview.html(response.body) - preview.syntaxHighlight() - @renderReferencedUsers(response.references.users, form) - - renderMarkdown: (text, success) -> - return unless window.markdown_preview_path - - return success(@ajaxCache.response) if text == @ajaxCache.text - - $.ajax - type: 'POST' - url: window.markdown_preview_path - data: { text: text } - dataType: 'json' - success: (response) => - @ajaxCache = text: text, response: response - success(response) - - hideReferencedUsers: (form) -> - referencedUsers = form.find('.referenced-users') - referencedUsers.hide() - - renderReferencedUsers: (users, form) -> - referencedUsers = form.find('.referenced-users') - - if referencedUsers.length - if users.length >= @referenceThreshold - referencedUsers.show() - referencedUsers.find('.js-referenced-users-count').text(users.length) - else - referencedUsers.hide() - -markdownPreview = new MarkdownPreview() - -previewButtonSelector = '.js-md-preview-button' -writeButtonSelector = '.js-md-write-button' -lastTextareaPreviewed = null - -$.fn.setupMarkdownPreview = -> - $form = $(this) - - form_textarea = $form.find('textarea.markdown-area') - - form_textarea.on 'input', -> markdownPreview.hideReferencedUsers($form) - form_textarea.on 'blur', -> markdownPreview.showPreview($form) - -$(document).on 'markdown-preview:show', (e, $form) -> - return unless $form - - lastTextareaPreviewed = $form.find('textarea.markdown-area') - - # toggle tabs - $form.find(writeButtonSelector).parent().removeClass('active') - $form.find(previewButtonSelector).parent().addClass('active') - - # toggle content - $form.find('.md-write-holder').hide() - $form.find('.md-preview-holder').show() - - markdownPreview.showPreview($form) - -$(document).on 'markdown-preview:hide', (e, $form) -> - return unless $form - - lastTextareaPreviewed = null - - # toggle tabs - $form.find(writeButtonSelector).parent().addClass('active') - $form.find(previewButtonSelector).parent().removeClass('active') - - # toggle content - $form.find('.md-write-holder').show() - $form.find('textarea.markdown-area').focus() - $form.find('.md-preview-holder').hide() - -$(document).on 'markdown-preview:toggle', (e, keyboardEvent) -> - $target = $(keyboardEvent.target) - - if $target.is('textarea.markdown-area') - $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]) - keyboardEvent.preventDefault() - else if lastTextareaPreviewed - $target = lastTextareaPreviewed - $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]) - keyboardEvent.preventDefault() - -$(document).on 'click', previewButtonSelector, (e) -> - e.preventDefault() - - $form = $(this).closest('form') - - $(document).triggerHandler('markdown-preview:show', [$form]) - -$(document).on 'click', writeButtonSelector, (e) -> - e.preventDefault() - - $form = $(this).closest('form') - - $(document).triggerHandler('markdown-preview:hide', [$form]) diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js new file mode 100644 index 00000000000..47e6dd1084d --- /dev/null +++ b/app/assets/javascripts/merge_request.js @@ -0,0 +1,105 @@ + +/*= require jquery.waitforimages */ + + +/*= require task_list */ + + +/*= require merge_request_tabs */ + +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.MergeRequest = (function() { + function MergeRequest(opts) { + this.opts = opts != null ? opts : {}; + this.submitNoteForm = bind(this.submitNoteForm, this); + this.$el = $('.merge-request'); + this.$('.show-all-commits').on('click', (function(_this) { + return function() { + return _this.showAllCommits(); + }; + })(this)); + this.initTabs(); + this.disableTaskList(); + this.initMRBtnListeners(); + if ($("a.btn-close").length) { + this.initTaskList(); + } + } + + MergeRequest.prototype.$ = function(selector) { + return this.$el.find(selector); + }; + + MergeRequest.prototype.initTabs = function() { + if (this.opts.action !== 'new') { + return new MergeRequestTabs(this.opts); + } else { + return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show'); + } + }; + + MergeRequest.prototype.showAllCommits = function() { + this.$('.first-commits').remove(); + 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; + return $('a.btn-close, a.btn-reopen').on('click', function(e) { + var $this, shouldSubmit; + $this = $(this); + shouldSubmit = $this.hasClass('btn-comment'); + if (shouldSubmit && $this.data('submitted')) { + return; + } + if (shouldSubmit) { + if ($this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')) { + e.preventDefault(); + e.stopImmediatePropagation(); + return _this.submitNoteForm($this.closest('form'), $this); + } + } + }); + }; + + MergeRequest.prototype.submitNoteForm = function(form, $button) { + var noteText; + noteText = form.find("textarea.js-note-text").val(); + if (noteText.trim().length > 0) { + form.submit(); + $button.data('submitted', true); + return $button.trigger('click'); + } + }; + + 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 + }); + }; + + return MergeRequest; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee deleted file mode 100644 index dabfd91cf14..00000000000 --- a/app/assets/javascripts/merge_request.js.coffee +++ /dev/null @@ -1,82 +0,0 @@ -#= require jquery.waitforimages -#= require task_list - -#= require merge_request_tabs - -class @MergeRequest - # Initialize MergeRequest behavior - # - # Options: - # action - String, current controller action - # - constructor: (@opts = {}) -> - this.$el = $('.merge-request') - - this.$('.show-all-commits').on 'click', => - this.showAllCommits() - - @initTabs() - - # Prevent duplicate event bindings - @disableTaskList() - @initMRBtnListeners() - - if $("a.btn-close").length - @initTaskList() - - # Local jQuery finder - $: (selector) -> - this.$el.find(selector) - - initTabs: -> - if @opts.action != 'new' - # `MergeRequests#new` has no tab-persisting or lazy-loading behavior - new MergeRequestTabs(@opts) - else - # Show the first tab (Commits) - $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show') - - showAllCommits: -> - this.$('.first-commits').remove() - this.$('.all-commits').removeClass 'hide' - - initTaskList: -> - $('.detail-page-description .js-task-list-container').taskList('enable') - $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList - - initMRBtnListeners: -> - _this = @ - $('a.btn-close, a.btn-reopen').on 'click', (e) -> - $this = $(this) - shouldSubmit = $this.hasClass('btn-comment') - if shouldSubmit && $this.data('submitted') - return - if shouldSubmit - if $this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen') - e.preventDefault() - e.stopImmediatePropagation() - _this.submitNoteForm($this.closest('form'),$this) - - - submitNoteForm: (form, $button) => - noteText = form.find("textarea.js-note-text").val() - if noteText.trim().length > 0 - form.submit() - $button.data('submitted',true) - $button.trigger('click') - - - disableTaskList: -> - $('.detail-page-description .js-task-list-container').taskList('disable') - $(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container' - - # TODO (rspeicher): Make the merge request description inline-editable like a - # note so that we can re-use its form here - updateTaskList: -> - patchData = {} - patchData['merge_request'] = {'description': $('.js-task-list-field', this).val()} - - $.ajax - type: 'PATCH' - url: $('form.js-issuable-update').attr('action') - data: patchData diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js new file mode 100644 index 00000000000..52c2ed61012 --- /dev/null +++ b/app/assets/javascripts/merge_request_tabs.js @@ -0,0 +1,239 @@ + +/*= require jquery.cookie */ + +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.MergeRequestTabs = (function() { + MergeRequestTabs.prototype.diffsLoaded = false; + + MergeRequestTabs.prototype.buildsLoaded = false; + + MergeRequestTabs.prototype.commitsLoaded = false; + + function MergeRequestTabs(opts) { + this.opts = opts != null ? opts : {}; + this.setCurrentAction = bind(this.setCurrentAction, this); + this.tabShown = bind(this.tabShown, this); + this.showTab = bind(this.showTab, this); + this._location = location; + this.bindEvents(); + this.activateTab(this.opts.action); + } + + MergeRequestTabs.prototype.bindEvents = function() { + $(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); + return $(document).on('click', '.js-show-tab', this.showTab); + }; + + MergeRequestTabs.prototype.showTab = function(event) { + event.preventDefault(); + return this.activateTab($(event.target).data('action')); + }; + + MergeRequestTabs.prototype.tabShown = function(event) { + var $target, action, navBarHeight; + $target = $(event.target); + action = $target.data('action'); + if (action === 'commits') { + this.loadCommits($target.attr('href')); + this.expandView(); + } else if (action === 'diffs') { + this.loadDiff($target.attr('href')); + if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') { + this.shrinkView(); + } + navBarHeight = $('.navbar-gitlab').outerHeight(); + $.scrollTo(".merge-request-details .merge-request-tabs", { + offset: -navBarHeight + }); + } else if (action === 'builds') { + this.loadBuilds($target.attr('href')); + this.expandView(); + } else { + this.expandView(); + } + return this.setCurrentAction(action); + }; + + MergeRequestTabs.prototype.scrollToElement = function(container) { + var $el, navBarHeight; + if (window.location.hash) { + navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); + $el = $(container + " " + window.location.hash + ":not(.match)"); + if ($el.length) { + return $.scrollTo(container + " " + window.location.hash + ":not(.match)", { + offset: -navBarHeight + }); + } + } + }; + + MergeRequestTabs.prototype.activateTab = function(action) { + if (action === 'show') { + action = 'notes'; + } + return $(".merge-request-tabs a[data-action='" + action + "']").tab('show'); + }; + + MergeRequestTabs.prototype.setCurrentAction = function(action) { + var new_state; + if (action === 'show') { + action = 'notes'; + } + new_state = this._location.pathname.replace(/\/(commits|diffs|builds)(\.html)?\/?$/, ''); + if (action !== 'notes') { + new_state += "/" + action; + } + new_state += this._location.search + this._location.hash; + history.replaceState({ + turbolinks: true, + url: new_state + }, document.title, new_state); + return new_state; + }; + + MergeRequestTabs.prototype.loadCommits = function(source) { + if (this.commitsLoaded) { + return; + } + return this._get({ + url: source + ".json", + success: (function(_this) { + return function(data) { + document.querySelector("div#commits").innerHTML = data.html; + gl.utils.localTimeAgo($('.js-timeago', 'div#commits')); + _this.commitsLoaded = true; + return _this.scrollToElement("#commits"); + }; + })(this) + }); + }; + + MergeRequestTabs.prototype.loadDiff = function(source) { + if (this.diffsLoaded) { + return; + } + return this._get({ + url: (source + ".json") + this._location.search, + success: (function(_this) { + return function(data) { + $('#diffs').html(data.html); + gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); + $('#diffs .js-syntax-highlight').syntaxHighlight(); + $('#diffs .diff-file').singleFileDiff(); + if (_this.diffViewType() === 'parallel') { + _this.expandViewContainer(); + } + _this.diffsLoaded = true; + _this.scrollToElement("#diffs"); + _this.highlighSelectedLine(); + _this.filesCommentButton = $('.files .diff-file').filesCommentButton(); + return $(document).off('click', '.diff-line-num a').on('click', '.diff-line-num a', function(e) { + e.preventDefault(); + window.location.hash = $(e.currentTarget).attr('href'); + _this.highlighSelectedLine(); + return _this.scrollToElement("#diffs"); + }); + }; + })(this) + }); + }; + + MergeRequestTabs.prototype.highlighSelectedLine = function() { + var $diffLine, diffLineTop, hashClassString, locationHash, navBarHeight; + $('.hll').removeClass('hll'); + locationHash = window.location.hash; + if (locationHash !== '') { + hashClassString = "." + (locationHash.replace('#', '')); + $diffLine = $(locationHash + ":not(.match)", $('#diffs')); + if (!$diffLine.is('tr')) { + $diffLine = $('#diffs').find("td" + locationHash + ", td" + hashClassString); + } else { + $diffLine = $diffLine.find('td'); + } + if ($diffLine.length) { + $diffLine.addClass('hll'); + diffLineTop = $diffLine.offset().top; + return navBarHeight = $('.navbar-gitlab').outerHeight(); + } + } + }; + + MergeRequestTabs.prototype.loadBuilds = function(source) { + if (this.buildsLoaded) { + return; + } + return this._get({ + url: source + ".json", + success: (function(_this) { + return function(data) { + document.querySelector("div#builds").innerHTML = data.html; + gl.utils.localTimeAgo($('.js-timeago', 'div#builds')); + _this.buildsLoaded = true; + return _this.scrollToElement("#builds"); + }; + })(this) + }); + }; + + MergeRequestTabs.prototype.toggleLoading = function(status) { + return $('.mr-loading-status .loading').toggle(status); + }; + + MergeRequestTabs.prototype._get = function(options) { + var defaults; + defaults = { + beforeSend: (function(_this) { + return function() { + return _this.toggleLoading(true); + }; + })(this), + complete: (function(_this) { + return function() { + return _this.toggleLoading(false); + }; + })(this), + dataType: 'json', + type: 'GET' + }; + options = $.extend({}, defaults, options); + return $.ajax(options); + }; + + MergeRequestTabs.prototype.diffViewType = function() { + return $('.inline-parallel-buttons a.active').data('view-type'); + }; + + MergeRequestTabs.prototype.expandViewContainer = function() { + return $('.container-fluid').removeClass('container-limited'); + }; + + MergeRequestTabs.prototype.shrinkView = function() { + var $gutterIcon; + $gutterIcon = $('.js-sidebar-toggle i:visible'); + return setTimeout(function() { + if ($gutterIcon.is('.fa-angle-double-right')) { + return $gutterIcon.closest('a').trigger('click', [true]); + } + }, 0); + }; + + MergeRequestTabs.prototype.expandView = function() { + var $gutterIcon; + if ($.cookie('collapsed_gutter') === 'true') { + return; + } + $gutterIcon = $('.js-sidebar-toggle i:visible'); + return setTimeout(function() { + if ($gutterIcon.is('.fa-angle-double-left')) { + return $gutterIcon.closest('a').trigger('click', [true]); + } + }, 0); + }; + + return MergeRequestTabs; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee deleted file mode 100644 index 86539e0d725..00000000000 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ /dev/null @@ -1,252 +0,0 @@ -# MergeRequestTabs -# -# Handles persisting and restoring the current tab selection and lazily-loading -# content on the MergeRequests#show page. -# -#= require jquery.cookie -# -# ### Example Markup -# -# <ul class="nav-links merge-request-tabs"> -# <li class="notes-tab active"> -# <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1"> -# Discussion -# </a> -# </li> -# <li class="commits-tab"> -# <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits"> -# Commits -# </a> -# </li> -# <li class="diffs-tab"> -# <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs"> -# Diffs -# </a> -# </li> -# </ul> -# -# <div class="tab-content"> -# <div class="notes tab-pane active" id="notes"> -# Notes Content -# </div> -# <div class="commits tab-pane" id="commits"> -# Commits Content -# </div> -# <div class="diffs tab-pane" id="diffs"> -# Diffs Content -# </div> -# </div> -# -# <div class="mr-loading-status"> -# <div class="loading"> -# Loading Animation -# </div> -# </div> -# -class @MergeRequestTabs - diffsLoaded: false - buildsLoaded: false - commitsLoaded: false - - constructor: (@opts = {}) -> - # Store the `location` object, allowing for easier stubbing in tests - @_location = location - - @bindEvents() - @activateTab(@opts.action) - - bindEvents: -> - $(document).on 'shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', @tabShown - $(document).on 'click', '.js-show-tab', @showTab - - showTab: (event) => - event.preventDefault() - - @activateTab $(event.target).data('action') - - tabShown: (event) => - $target = $(event.target) - action = $target.data('action') - - if action == 'commits' - @loadCommits($target.attr('href')) - @expandView() - else if action == 'diffs' - @loadDiff($target.attr('href')) - if bp? and bp.getBreakpointSize() isnt 'lg' - @shrinkView() - - navBarHeight = $('.navbar-gitlab').outerHeight() - $.scrollTo(".merge-request-details .merge-request-tabs", offset: -navBarHeight) - else if action == 'builds' - @loadBuilds($target.attr('href')) - @expandView() - else - @expandView() - - @setCurrentAction(action) - - scrollToElement: (container) -> - if window.location.hash - navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() - - $el = $("#{container} #{window.location.hash}:not(.match)") - $.scrollTo("#{container} #{window.location.hash}:not(.match)", offset: -navBarHeight) if $el.length - - # Activate a tab based on the current action - activateTab: (action) -> - action = 'notes' if action == 'show' - $(".merge-request-tabs a[data-action='#{action}']").tab('show') - - # Replaces the current Merge Request-specific action in the URL with a new one - # - # If the action is "notes", the URL is reset to the standard - # `MergeRequests#show` route. - # - # Examples: - # - # location.pathname # => "/namespace/project/merge_requests/1" - # setCurrentAction('diffs') - # location.pathname # => "/namespace/project/merge_requests/1/diffs" - # - # location.pathname # => "/namespace/project/merge_requests/1/diffs" - # setCurrentAction('notes') - # location.pathname # => "/namespace/project/merge_requests/1" - # - # location.pathname # => "/namespace/project/merge_requests/1/diffs" - # setCurrentAction('commits') - # location.pathname # => "/namespace/project/merge_requests/1/commits" - # - # Returns the new URL String - setCurrentAction: (action) => - # Normalize action, just to be safe - action = 'notes' if action == 'show' - - # Remove a trailing '/commits' or '/diffs' - new_state = @_location.pathname.replace(/\/(commits|diffs|builds)(\.html)?\/?$/, '') - - # Append the new action if we're on a tab other than 'notes' - unless action == 'notes' - new_state += "/#{action}" - - # Ensure parameters and hash come along for the ride - new_state += @_location.search + @_location.hash - - # Replace the current history state with the new one without breaking - # Turbolinks' history. - # - # See https://github.com/rails/turbolinks/issues/363 - history.replaceState {turbolinks: true, url: new_state}, document.title, new_state - - new_state - - loadCommits: (source) -> - return if @commitsLoaded - - @_get - url: "#{source}.json" - success: (data) => - document.querySelector("div#commits").innerHTML = data.html - gl.utils.localTimeAgo($('.js-timeago', 'div#commits')) - @commitsLoaded = true - @scrollToElement("#commits") - - loadDiff: (source) -> - return if @diffsLoaded - @_get - url: "#{source}.json" + @_location.search - success: (data) => - $('#diffs').html data.html - gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')) - $('#diffs .js-syntax-highlight').syntaxHighlight() - $('#diffs .diff-file').singleFileDiff() - @expandViewContainer() if @diffViewType() is 'parallel' - @diffsLoaded = true - @scrollToElement("#diffs") - @highlighSelectedLine() - @filesCommentButton = $('.files .diff-file').filesCommentButton() - - $(document) - .off 'click', '.diff-line-num a' - .on 'click', '.diff-line-num a', (e) => - e.preventDefault() - window.location.hash = $(e.currentTarget).attr 'href' - @highlighSelectedLine() - @scrollToElement("#diffs") - - highlighSelectedLine: -> - $('.hll').removeClass 'hll' - locationHash = window.location.hash - - if locationHash isnt '' - hashClassString = ".#{locationHash.replace('#', '')}" - $diffLine = $("#{locationHash}:not(.match)", $('#diffs')) - - if not $diffLine.is 'tr' - $diffLine = $('#diffs').find("td#{locationHash}, td#{hashClassString}") - else - $diffLine = $diffLine.find('td') - - if $diffLine.length - $diffLine.addClass 'hll' - diffLineTop = $diffLine.offset().top - navBarHeight = $('.navbar-gitlab').outerHeight() - - loadBuilds: (source) -> - return if @buildsLoaded - - @_get - url: "#{source}.json" - success: (data) => - document.querySelector("div#builds").innerHTML = data.html - gl.utils.localTimeAgo($('.js-timeago', 'div#builds')) - @buildsLoaded = true - @scrollToElement("#builds") - - # Show or hide the loading spinner - # - # status - Boolean, true to show, false to hide - toggleLoading: (status) -> - $('.mr-loading-status .loading').toggle(status) - - _get: (options) -> - defaults = { - beforeSend: => @toggleLoading(true) - complete: => @toggleLoading(false) - dataType: 'json' - type: 'GET' - } - - options = $.extend({}, defaults, options) - - $.ajax(options) - - # Returns diff view type - diffViewType: -> - $('.inline-parallel-buttons a.active').data('view-type') - - expandViewContainer: -> - $('.container-fluid').removeClass('container-limited') - - shrinkView: -> - $gutterIcon = $('.js-sidebar-toggle i:visible') - - # Wait until listeners are set - setTimeout( -> - # Only when sidebar is expanded - if $gutterIcon.is('.fa-angle-double-right') - $gutterIcon.closest('a').trigger('click', [true]) - , 0) - - # Expand the issuable sidebar unless the user explicitly collapsed it - expandView: -> - return if $.cookie('collapsed_gutter') == 'true' - - $gutterIcon = $('.js-sidebar-toggle i:visible') - - # Wait until listeners are set - setTimeout( -> - # Only when sidebar is collapsed - if $gutterIcon.is('.fa-angle-double-left') - $gutterIcon.closest('a').trigger('click', [true]) - , 0) diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js new file mode 100644 index 00000000000..362aaa906d0 --- /dev/null +++ b/app/assets/javascripts/merge_request_widget.js @@ -0,0 +1,185 @@ +(function() { + var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + + this.MergeRequestWidget = (function() { + function MergeRequestWidget(opts) { + this.opts = opts; + $('#modal_merge_info').modal({ + show: false + }); + this.firstCICheck = true; + this.readyForCICheck = false; + this.cancel = false; + clearInterval(this.fetchBuildStatusInterval); + this.clearEventListeners(); + this.addEventListeners(); + this.getCIStatus(false); + this.pollCIStatus(); + notifyPermissions(); + } + + MergeRequestWidget.prototype.clearEventListeners = function() { + return $(document).off('page:change.merge_request'); + }; + + MergeRequestWidget.prototype.cancelPolling = function() { + return this.cancel = true; + }; + + MergeRequestWidget.prototype.addEventListeners = function() { + var allowedPages; + allowedPages = ['show', 'commits', 'builds', 'changes']; + return $(document).on('page:change.merge_request', (function(_this) { + return function() { + var page; + page = $('body').data('page').split(':').last(); + if (allowedPages.indexOf(page) < 0) { + clearInterval(_this.fetchBuildStatusInterval); + _this.cancelPolling(); + return _this.clearEventListeners(); + } + }; + })(this)); + }; + + MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) { + if (deleteSourceBranch == null) { + deleteSourceBranch = false; + } + return $.ajax({ + type: 'GET', + url: $('.merge-request').data('url'), + success: (function(_this) { + return function(data) { + var callback, urlSuffix; + if (data.state === "merged") { + urlSuffix = deleteSourceBranch ? '?delete_source=true' : ''; + return window.location.href = window.location.pathname + urlSuffix; + } else if (data.merge_error) { + return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>"); + } else { + callback = function() { + return merge_request_widget.mergeInProgress(deleteSourceBranch); + }; + return setTimeout(callback, 2000); + } + }; + })(this), + dataType: 'json' + }); + }; + + MergeRequestWidget.prototype.getMergeStatus = function() { + return $.get(this.opts.merge_check_url, function(data) { + return $('.mr-state-widget').replaceWith(data); + }); + }; + + MergeRequestWidget.prototype.ciLabelForStatus = function(status) { + switch (status) { + case 'success': + return 'passed'; + case 'success_with_warnings': + return 'passed with warnings'; + default: + return status; + } + }; + + MergeRequestWidget.prototype.pollCIStatus = function() { + return this.fetchBuildStatusInterval = setInterval(((function(_this) { + return function() { + if (!_this.readyForCICheck) { + return; + } + _this.getCIStatus(true); + return _this.readyForCICheck = false; + }; + })(this)), 10000); + }; + + MergeRequestWidget.prototype.getCIStatus = function(showNotification) { + var _this; + _this = this; + $('.ci-widget-fetching').show(); + return $.getJSON(this.opts.ci_status_url, (function(_this) { + return function(data) { + var message, status, title; + if (_this.cancel) { + return; + } + _this.readyForCICheck = true; + if (data.status === '') { + return; + } + if (_this.firstCICheck || data.status !== _this.opts.ci_status && (data.status != null)) { + _this.opts.ci_status = data.status; + _this.showCIStatus(data.status); + if (data.coverage) { + _this.showCICoverage(data.coverage); + } + if (showNotification && !_this.firstCICheck) { + status = _this.ciLabelForStatus(data.status); + if (status === "preparing") { + title = _this.opts.ci_title.preparing; + status = status.charAt(0).toUpperCase() + status.slice(1); + message = _this.opts.ci_message.preparing.replace('{{status}}', status); + } else { + title = _this.opts.ci_title.normal; + message = _this.opts.ci_message.normal.replace('{{status}}', status); + } + title = title.replace('{{status}}', status); + message = message.replace('{{sha}}', data.sha); + message = message.replace('{{title}}', data.title); + notify(title, message, _this.opts.gitlab_icon, function() { + this.close(); + return Turbolinks.visit(_this.opts.builds_path); + }); + } + return _this.firstCICheck = false; + } + }; + })(this)); + }; + + MergeRequestWidget.prototype.showCIStatus = function(state) { + var allowed_states; + if (state == null) { + return; + } + $('.ci_widget').hide(); + allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"]; + if (indexOf.call(allowed_states, state) >= 0) { + $('.ci_widget.ci-' + state).show(); + switch (state) { + case "failed": + case "canceled": + case "not_found": + return this.setMergeButtonClass('btn-danger'); + case "running": + return this.setMergeButtonClass('btn-warning'); + case "success": + case "success_with_warnings": + return this.setMergeButtonClass('btn-create'); + } + } else { + $('.ci_widget.ci-error').show(); + return this.setMergeButtonClass('btn-danger'); + } + }; + + MergeRequestWidget.prototype.showCICoverage = function(coverage) { + var text; + text = 'Coverage ' + coverage + '%'; + return $('.ci_widget:visible .ci-coverage').text(text); + }; + + MergeRequestWidget.prototype.setMergeButtonClass = function(css_class) { + return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-warning btn-create').addClass(css_class); + }; + + return MergeRequestWidget; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee deleted file mode 100644 index 963a0550c35..00000000000 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ /dev/null @@ -1,143 +0,0 @@ -class @MergeRequestWidget - # Initialize MergeRequestWidget behavior - # - # check_enable - Boolean, whether to check automerge status - # merge_check_url - String, URL to use to check automerge status - # ci_status_url - String, URL to use to check CI status - # - - constructor: (@opts) -> - $('#modal_merge_info').modal(show: false) - @firstCICheck = true - @readyForCICheck = false - @cancel = false - clearInterval @fetchBuildStatusInterval - - @clearEventListeners() - @addEventListeners() - @getCIStatus(false) - @pollCIStatus() - notifyPermissions() - - clearEventListeners: -> - $(document).off 'page:change.merge_request' - - cancelPolling: -> - @cancel = true - - addEventListeners: -> - allowedPages = ['show', 'commits', 'builds', 'changes'] - $(document).on 'page:change.merge_request', => - page = $('body').data('page').split(':').last() - if allowedPages.indexOf(page) < 0 - clearInterval @fetchBuildStatusInterval - @cancelPolling() - @clearEventListeners() - - mergeInProgress: (deleteSourceBranch = false)-> - $.ajax - type: 'GET' - url: $('.merge-request').data('url') - success: (data) => - if data.state == "merged" - urlSuffix = if deleteSourceBranch then '?delete_source=true' else '' - - window.location.href = window.location.pathname + urlSuffix - else if data.merge_error - $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>") - else - callback = -> merge_request_widget.mergeInProgress(deleteSourceBranch) - setTimeout(callback, 2000) - dataType: 'json' - - getMergeStatus: -> - $.get @opts.merge_check_url, (data) -> - $('.mr-state-widget').replaceWith(data) - - ciLabelForStatus: (status) -> - switch status - when 'success' - 'passed' - when 'success_with_warnings' - 'passed with warnings' - else - status - - pollCIStatus: -> - @fetchBuildStatusInterval = setInterval ( => - return if not @readyForCICheck - - @getCIStatus(true) - - @readyForCICheck = false - ), 10000 - - getCIStatus: (showNotification) -> - _this = @ - $('.ci-widget-fetching').show() - - $.getJSON @opts.ci_status_url, (data) => - return if @cancel - @readyForCICheck = true - - if data.status is '' - return - - if @firstCICheck || data.status isnt @opts.ci_status and data.status? - @opts.ci_status = data.status - @showCIStatus data.status - if data.coverage - @showCICoverage data.coverage - - # The first check should only update the UI, a notification - # should only be displayed on status changes - if showNotification and not @firstCICheck - status = @ciLabelForStatus(data.status) - - if status is "preparing" - title = @opts.ci_title.preparing - status = status.charAt(0).toUpperCase() + status.slice(1); - message = @opts.ci_message.preparing.replace('{{status}}', status) - else - title = @opts.ci_title.normal - message = @opts.ci_message.normal.replace('{{status}}', status) - - title = title.replace('{{status}}', status) - message = message.replace('{{sha}}', data.sha) - message = message.replace('{{title}}', data.title) - - notify( - title, - message, - @opts.gitlab_icon, - -> - @close() - Turbolinks.visit _this.opts.builds_path - ) - @firstCICheck = false - - showCIStatus: (state) -> - return if not state? - $('.ci_widget').hide() - allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"] - if state in allowed_states - $('.ci_widget.ci-' + state).show() - switch state - when "failed", "canceled", "not_found" - @setMergeButtonClass('btn-danger') - when "running" - @setMergeButtonClass('btn-warning') - when "success", "success_with_warnings" - @setMergeButtonClass('btn-create') - else - $('.ci_widget.ci-error').show() - @setMergeButtonClass('btn-danger') - - showCICoverage: (coverage) -> - text = 'Coverage ' + coverage + '%' - $('.ci_widget:visible .ci-coverage').text(text) - - setMergeButtonClass: (css_class) -> - $('.js-merge-button,.accept-action .dropdown-toggle') - .removeClass('btn-danger btn-warning btn-create') - .addClass(css_class) diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js new file mode 100644 index 00000000000..1fed38661a2 --- /dev/null +++ b/app/assets/javascripts/merged_buttons.js @@ -0,0 +1,45 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.MergedButtons = (function() { + function MergedButtons() { + this.removeSourceBranch = bind(this.removeSourceBranch, this); + this.$removeBranchWidget = $('.remove_source_branch_widget'); + this.$removeBranchProgress = $('.remove_source_branch_in_progress'); + this.$removeBranchFailed = $('.remove_source_branch_widget.failed'); + this.cleanEventListeners(); + this.initEventListeners(); + } + + MergedButtons.prototype.cleanEventListeners = function() { + $(document).off('click', '.remove_source_branch'); + $(document).off('ajax:success', '.remove_source_branch'); + return $(document).off('ajax:error', '.remove_source_branch'); + }; + + MergedButtons.prototype.initEventListeners = function() { + $(document).on('click', '.remove_source_branch', this.removeSourceBranch); + $(document).on('ajax:success', '.remove_source_branch', this.removeBranchSuccess); + return $(document).on('ajax:error', '.remove_source_branch', this.removeBranchError); + }; + + MergedButtons.prototype.removeSourceBranch = function() { + this.$removeBranchWidget.hide(); + return this.$removeBranchProgress.show(); + }; + + MergedButtons.prototype.removeBranchSuccess = function() { + return location.reload(); + }; + + MergedButtons.prototype.removeBranchError = function() { + this.$removeBranchWidget.hide(); + this.$removeBranchProgress.hide(); + return this.$removeBranchFailed.show(); + }; + + return MergedButtons; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/merged_buttons.js.coffee b/app/assets/javascripts/merged_buttons.js.coffee deleted file mode 100644 index 4929295c10b..00000000000 --- a/app/assets/javascripts/merged_buttons.js.coffee +++ /dev/null @@ -1,30 +0,0 @@ -class @MergedButtons - constructor: -> - @$removeBranchWidget = $('.remove_source_branch_widget') - @$removeBranchProgress = $('.remove_source_branch_in_progress') - @$removeBranchFailed = $('.remove_source_branch_widget.failed') - - @cleanEventListeners() - @initEventListeners() - - cleanEventListeners: -> - $(document).off 'click', '.remove_source_branch' - $(document).off 'ajax:success', '.remove_source_branch' - $(document).off 'ajax:error', '.remove_source_branch' - - initEventListeners: -> - $(document).on 'click', '.remove_source_branch', @removeSourceBranch - $(document).on 'ajax:success', '.remove_source_branch', @removeBranchSuccess - $(document).on 'ajax:error', '.remove_source_branch', @removeBranchError - - removeSourceBranch: => - @$removeBranchWidget.hide() - @$removeBranchProgress.show() - - removeBranchSuccess: -> - location.reload() - - removeBranchError: -> - @$removeBranchWidget.hide() - @$removeBranchProgress.hide() - @$removeBranchFailed.show() diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js new file mode 100644 index 00000000000..e8d51da7d58 --- /dev/null +++ b/app/assets/javascripts/milestone.js @@ -0,0 +1,195 @@ +(function() { + this.Milestone = (function() { + Milestone.updateIssue = function(li, issue_url, data) { + return $.ajax({ + type: "PUT", + url: issue_url, + data: data, + success: (function(_this) { + return function(_data) { + return _this.successCallback(_data, li); + }; + })(this), + error: function(data) { + return new Flash("Issue update failed", 'alert'); + }, + dataType: "json" + }); + }; + + Milestone.sortIssues = function(data) { + var sort_issues_url; + sort_issues_url = location.href + "/sort_issues"; + return $.ajax({ + type: "PUT", + url: sort_issues_url, + data: data, + success: (function(_this) { + return function(_data) { + return _this.successCallback(_data); + }; + })(this), + error: function() { + return new Flash("Issues update failed", 'alert'); + }, + dataType: "json" + }); + }; + + Milestone.sortMergeRequests = function(data) { + var sort_mr_url; + sort_mr_url = location.href + "/sort_merge_requests"; + return $.ajax({ + type: "PUT", + url: sort_mr_url, + data: data, + success: (function(_this) { + return function(_data) { + return _this.successCallback(_data); + }; + })(this), + error: function(data) { + return new Flash("Issue update failed", 'alert'); + }, + dataType: "json" + }); + }; + + Milestone.updateMergeRequest = function(li, merge_request_url, data) { + return $.ajax({ + type: "PUT", + url: merge_request_url, + data: data, + success: (function(_this) { + return function(_data) { + return _this.successCallback(_data, li); + }; + })(this), + error: function(data) { + return new Flash("Issue update failed", 'alert'); + }, + dataType: "json" + }); + }; + + Milestone.successCallback = function(data, element) { + var img_tag; + if (data.assignee) { + img_tag = $('<img/>'); + img_tag.attr('src', data.assignee.avatar_url); + img_tag.addClass('avatar s16'); + $(element).find('.assignee-icon').html(img_tag); + } else { + $(element).find('.assignee-icon').html(''); + } + return $(element).effect('highlight'); + }; + + function Milestone() { + var oldMouseStart; + oldMouseStart = $.ui.sortable.prototype._mouseStart; + $.ui.sortable.prototype._mouseStart = function(event, overrideHandle, noActivation) { + this._trigger("beforeStart", event, this._uiHash()); + return oldMouseStart.apply(this, [event, overrideHandle, noActivation]); + }; + this.bindIssuesSorting(); + this.bindMergeRequestSorting(); + this.bindTabsSwitching(); + } + + Milestone.prototype.bindIssuesSorting = function() { + return $("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable({ + connectWith: ".issues-sortable-list", + dropOnEmpty: true, + items: "li:not(.ui-sort-disabled)", + beforeStart: function(event, ui) { + return $(".issues-sortable-list").css("min-height", ui.item.outerHeight()); + }, + stop: function(event, ui) { + return $(".issues-sortable-list").css("min-height", "0px"); + }, + update: function(event, ui) { + var data; + if ($(this).find(ui.item).length > 0) { + data = $(this).sortable("serialize"); + return Milestone.sortIssues(data); + } + }, + receive: function(event, ui) { + var data, issue_id, issue_url, new_state; + new_state = $(this).data('state'); + issue_id = ui.item.data('iid'); + issue_url = ui.item.data('url'); + data = (function() { + switch (new_state) { + case 'ongoing': + return "issue[assignee_id]=" + gon.current_user_id; + case 'unassigned': + return "issue[assignee_id]="; + case 'closed': + return "issue[state_event]=close"; + } + })(); + if ($(ui.sender).data('state') === "closed") { + data += "&issue[state_event]=reopen"; + } + return Milestone.updateIssue(ui.item, issue_url, data); + } + }).disableSelection(); + }; + + Milestone.prototype.bindTabsSwitching = function() { + return $('a[data-toggle="tab"]').on('show.bs.tab', function(e) { + var currentTabClass, previousTabClass; + currentTabClass = $(e.target).data('show'); + previousTabClass = $(e.relatedTarget).data('show'); + $(previousTabClass).hide(); + $(currentTabClass).removeClass('hidden'); + return $(currentTabClass).show(); + }); + }; + + Milestone.prototype.bindMergeRequestSorting = function() { + return $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable({ + connectWith: ".merge_requests-sortable-list", + dropOnEmpty: true, + items: "li:not(.ui-sort-disabled)", + beforeStart: function(event, ui) { + return $(".merge_requests-sortable-list").css("min-height", ui.item.outerHeight()); + }, + stop: function(event, ui) { + return $(".merge_requests-sortable-list").css("min-height", "0px"); + }, + update: function(event, ui) { + var data; + data = $(this).sortable("serialize"); + return Milestone.sortMergeRequests(data); + }, + receive: function(event, ui) { + var data, merge_request_id, merge_request_url, new_state; + new_state = $(this).data('state'); + merge_request_id = ui.item.data('iid'); + merge_request_url = ui.item.data('url'); + data = (function() { + switch (new_state) { + case 'ongoing': + return "merge_request[assignee_id]=" + gon.current_user_id; + case 'unassigned': + return "merge_request[assignee_id]="; + case 'closed': + return "merge_request[state_event]=close"; + } + })(); + if ($(ui.sender).data('state') === "closed") { + data += "&merge_request[state_event]=reopen"; + } + return Milestone.updateMergeRequest(ui.item, merge_request_url, data); + } + }).disableSelection(); + }; + + return Milestone; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee deleted file mode 100644 index a19e68b39e2..00000000000 --- a/app/assets/javascripts/milestone.js.coffee +++ /dev/null @@ -1,146 +0,0 @@ -class @Milestone - @updateIssue: (li, issue_url, data) -> - $.ajax - type: "PUT" - url: issue_url - data: data - success: (_data) => - @successCallback(_data, li) - error: (data) -> - new Flash("Issue update failed", 'alert') - dataType: "json" - - @sortIssues: (data) -> - sort_issues_url = location.href + "/sort_issues" - - $.ajax - type: "PUT" - url: sort_issues_url - data: data - success: (_data) => - @successCallback(_data) - error: -> - new Flash("Issues update failed", 'alert') - dataType: "json" - - @sortMergeRequests: (data) -> - sort_mr_url = location.href + "/sort_merge_requests" - - $.ajax - type: "PUT" - url: sort_mr_url - data: data - success: (_data) => - @successCallback(_data) - error: (data) -> - new Flash("Issue update failed", 'alert') - dataType: "json" - - @updateMergeRequest: (li, merge_request_url, data) -> - $.ajax - type: "PUT" - url: merge_request_url - data: data - success: (_data) => - @successCallback(_data, li) - error: (data) -> - new Flash("Issue update failed", 'alert') - dataType: "json" - - @successCallback: (data, element) => - if data.assignee - img_tag = $('<img/>') - img_tag.attr('src', data.assignee.avatar_url) - img_tag.addClass('avatar s16') - $(element).find('.assignee-icon').html(img_tag) - else - $(element).find('.assignee-icon').html('') - - $(element).effect 'highlight' - - constructor: -> - oldMouseStart = $.ui.sortable.prototype._mouseStart - $.ui.sortable.prototype._mouseStart = (event, overrideHandle, noActivation) -> - this._trigger "beforeStart", event, this._uiHash() - oldMouseStart.apply this, [event, overrideHandle, noActivation] - - @bindIssuesSorting() - @bindMergeRequestSorting() - @bindTabsSwitching() - - bindIssuesSorting: -> - $("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable( - connectWith: ".issues-sortable-list", - dropOnEmpty: true, - items: "li:not(.ui-sort-disabled)", - beforeStart: (event, ui) -> - $(".issues-sortable-list").css "min-height", ui.item.outerHeight() - stop: (event, ui) -> - $(".issues-sortable-list").css "min-height", "0px" - update: (event, ui) -> - # Prevents sorting from container which element has been removed. - if $(this).find(ui.item).length > 0 - data = $(this).sortable("serialize") - Milestone.sortIssues(data) - - receive: (event, ui) -> - new_state = $(this).data('state') - issue_id = ui.item.data('iid') - issue_url = ui.item.data('url') - - data = switch new_state - when 'ongoing' - "issue[assignee_id]=" + gon.current_user_id - when 'unassigned' - "issue[assignee_id]=" - when 'closed' - "issue[state_event]=close" - - if $(ui.sender).data('state') == "closed" - data += "&issue[state_event]=reopen" - - Milestone.updateIssue(ui.item, issue_url, data) - - ).disableSelection() - - bindTabsSwitching: -> - $('a[data-toggle="tab"]').on 'show.bs.tab', (e) -> - currentTabClass = $(e.target).data('show') - previousTabClass = $(e.relatedTarget).data('show') - - $(previousTabClass).hide() - $(currentTabClass).removeClass('hidden') - $(currentTabClass).show() - - bindMergeRequestSorting: -> - $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable( - connectWith: ".merge_requests-sortable-list", - dropOnEmpty: true, - items: "li:not(.ui-sort-disabled)", - beforeStart: (event, ui) -> - $(".merge_requests-sortable-list").css "min-height", ui.item.outerHeight() - stop: (event, ui) -> - $(".merge_requests-sortable-list").css "min-height", "0px" - update: (event, ui) -> - data = $(this).sortable("serialize") - Milestone.sortMergeRequests(data) - - receive: (event, ui) -> - new_state = $(this).data('state') - merge_request_id = ui.item.data('iid') - merge_request_url = ui.item.data('url') - - data = switch new_state - when 'ongoing' - "merge_request[assignee_id]=" + gon.current_user_id - when 'unassigned' - "merge_request[assignee_id]=" - when 'closed' - "merge_request[state_event]=close" - - if $(ui.sender).data('state') == "closed" - data += "&merge_request[state_event]=reopen" - - Milestone.updateMergeRequest(ui.item, merge_request_url, data) - - ).disableSelection() diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js new file mode 100644 index 00000000000..a0b65d20c03 --- /dev/null +++ b/app/assets/javascripts/milestone_select.js @@ -0,0 +1,151 @@ +(function() { + this.MilestoneSelect = (function() { + function MilestoneSelect(currentProject) { + var _this; + if (currentProject != null) { + _this = this; + this.currentProject = JSON.parse(currentProject); + } + $('.js-milestone-select').each(function(i, dropdown) { + var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId; + $dropdown = $(dropdown); + projectId = $dropdown.data('project-id'); + milestonesUrl = $dropdown.data('milestones'); + issueUpdateURL = $dropdown.data('issueUpdate'); + selectedMilestone = $dropdown.data('selected'); + showNo = $dropdown.data('show-no'); + showAny = $dropdown.data('show-any'); + showUpcoming = $dropdown.data('show-upcoming'); + useId = $dropdown.data('use-id'); + defaultLabel = $dropdown.data('default-label'); + issuableId = $dropdown.data('issuable-id'); + abilityName = $dropdown.data('ability-name'); + $selectbox = $dropdown.closest('.selectbox'); + $block = $selectbox.closest('.block'); + $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon'); + $value = $block.find('.value'); + $loading = $block.find('.block-loading').fadeOut(); + if (issueUpdateURL) { + milestoneLinkTemplate = _.template('<a href="/<%- namespace %>/<%- path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>'); + milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; + collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>'); + } + return $dropdown.glDropdown({ + data: function(term, callback) { + return $.ajax({ + url: milestonesUrl + }).done(function(data) { + var extraOptions; + extraOptions = []; + if (showAny) { + extraOptions.push({ + id: 0, + name: '', + title: 'Any Milestone' + }); + } + if (showNo) { + extraOptions.push({ + id: -1, + name: 'No Milestone', + title: 'No Milestone' + }); + } + if (showUpcoming) { + extraOptions.push({ + id: -2, + name: '#upcoming', + title: 'Upcoming' + }); + } + if (extraOptions.length > 2) { + extraOptions.push('divider'); + } + return callback(extraOptions.concat(data)); + }); + }, + filterable: true, + search: { + fields: ['title'] + }, + selectable: true, + toggleLabel: function(selected) { + if (selected && 'id' in selected) { + return selected.title; + } else { + return defaultLabel; + } + }, + fieldName: $dropdown.data('field-name'), + text: function(milestone) { + return _.escape(milestone.title); + }, + id: function(milestone) { + if (!useId) { + return milestone.name; + } else { + return milestone.id; + } + }, + isSelected: function(milestone) { + return milestone.name === selectedMilestone; + }, + hidden: function() { + $selectbox.hide(); + return $value.css('display', ''); + }, + clicked: function(selected) { + var data, isIssueIndex, isMRIndex, page; + page = $('body').data('page'); + isIssueIndex = page === 'projects:issues:index'; + isMRIndex = (page === page && page === 'projects:merge_requests:index'); + if ($dropdown.hasClass('js-filter-bulk-update')) { + return; + } + if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { + if (selected.name != null) { + selectedMilestone = selected.name; + } else { + selectedMilestone = ''; + } + return Issuable.filterResults($dropdown.closest('form')); + } else if ($dropdown.hasClass('js-filter-submit')) { + return $dropdown.closest('form').submit(); + } else { + selected = $selectbox.find('input[type="hidden"]').val(); + data = {}; + data[abilityName] = {}; + data[abilityName].milestone_id = selected != null ? selected : null; + $loading.fadeIn(); + $dropdown.trigger('loading.gl.dropdown'); + return $.ajax({ + type: 'PUT', + url: issueUpdateURL, + data: data + }).done(function(data) { + $dropdown.trigger('loaded.gl.dropdown'); + $loading.fadeOut(); + $selectbox.hide(); + $value.css('display', ''); + if (data.milestone != null) { + data.milestone.namespace = _this.currentProject.namespace; + data.milestone.path = _this.currentProject.path; + data.milestone.remaining = $.timefor(data.milestone.due_date); + $value.html(milestoneLinkTemplate(data.milestone)); + return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)); + } else { + $value.html(milestoneLinkNoneTemplate); + return $sidebarCollapsedValue.find('span').text('No'); + } + }); + } + } + }); + }); + } + + return MilestoneSelect; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee deleted file mode 100644 index 8ab03ed93ee..00000000000 --- a/app/assets/javascripts/milestone_select.js.coffee +++ /dev/null @@ -1,137 +0,0 @@ -class @MilestoneSelect - constructor: (currentProject) -> - if currentProject? - _this = @ - @currentProject = JSON.parse(currentProject) - $('.js-milestone-select').each (i, dropdown) -> - $dropdown = $(dropdown) - projectId = $dropdown.data('project-id') - milestonesUrl = $dropdown.data('milestones') - issueUpdateURL = $dropdown.data('issueUpdate') - selectedMilestone = $dropdown.data('selected') - showNo = $dropdown.data('show-no') - showAny = $dropdown.data('show-any') - showUpcoming = $dropdown.data('show-upcoming') - useId = $dropdown.data('use-id') - defaultLabel = $dropdown.data('default-label') - issuableId = $dropdown.data('issuable-id') - abilityName = $dropdown.data('ability-name') - $selectbox = $dropdown.closest('.selectbox') - $block = $selectbox.closest('.block') - $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon') - $value = $block.find('.value') - $loading = $block.find('.block-loading').fadeOut() - - if issueUpdateURL - milestoneLinkTemplate = _.template( - '<a href="/<%- namespace %>/<%- path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>' - ) - - milestoneLinkNoneTemplate = '<span class="no-value">None</span>' - - collapsedSidebarLabelTemplate = _.template( - '<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> - <%- title %> - </span>' - ) - - $dropdown.glDropdown( - data: (term, callback) -> - $.ajax( - url: milestonesUrl - ).done (data) -> - extraOptions = [] - if showAny - extraOptions.push( - id: 0 - name: '' - title: 'Any Milestone' - ) - - if showNo - extraOptions.push( - id: -1 - name: 'No Milestone' - title: 'No Milestone' - ) - - if showUpcoming - extraOptions.push( - id: -2 - name: '#upcoming' - title: 'Upcoming' - ) - - if extraOptions.length > 2 - extraOptions.push 'divider' - - callback(extraOptions.concat(data)) - filterable: true - search: - fields: ['title'] - selectable: true - toggleLabel: (selected) -> - if selected && 'id' of selected - selected.title - else - defaultLabel - fieldName: $dropdown.data('field-name') - text: (milestone) -> - _.escape(milestone.title) - id: (milestone) -> - if !useId - milestone.name - else - milestone.id - isSelected: (milestone) -> - milestone.name is selectedMilestone - hidden: -> - $selectbox.hide() - - # display:block overrides the hide-collapse rule - $value.css('display', '') - clicked: (selected) -> - page = $('body').data 'page' - isIssueIndex = page is 'projects:issues:index' - isMRIndex = page is page is 'projects:merge_requests:index' - - if $dropdown.hasClass 'js-filter-bulk-update' - return - - if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) - if selected.name? - selectedMilestone = selected.name - else - selectedMilestone = '' - Issuable.filterResults $dropdown.closest('form') - else if $dropdown.hasClass('js-filter-submit') - $dropdown.closest('form').submit() - else - selected = $selectbox - .find('input[type="hidden"]') - .val() - data = {} - data[abilityName] = {} - data[abilityName].milestone_id = if selected? then selected else null - $loading - .fadeIn() - $dropdown.trigger('loading.gl.dropdown') - $.ajax( - type: 'PUT' - url: issueUpdateURL - data: data - ).done (data) -> - $dropdown.trigger('loaded.gl.dropdown') - $loading.fadeOut() - $selectbox.hide() - $value.css('display', '') - if data.milestone? - data.milestone.namespace = _this.currentProject.namespace - data.milestone.path = _this.currentProject.path - data.milestone.remaining = $.timefor data.milestone.due_date - $value.html(milestoneLinkTemplate(data.milestone)) - $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)) - else - $value.html(milestoneLinkNoneTemplate) - $sidebarCollapsedValue.find('span').text('No') - ) diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js new file mode 100644 index 00000000000..10f4fd106d8 --- /dev/null +++ b/app/assets/javascripts/namespace_select.js @@ -0,0 +1,86 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.NamespaceSelect = (function() { + function NamespaceSelect(opts) { + this.onSelectItem = bind(this.onSelectItem, this); + var fieldName, showAny; + this.dropdown = opts.dropdown; + showAny = true; + fieldName = 'namespace_id'; + if (this.dropdown.attr('data-field-name')) { + fieldName = this.dropdown.data('fieldName'); + } + if (this.dropdown.attr('data-show-any')) { + showAny = this.dropdown.data('showAny'); + } + this.dropdown.glDropdown({ + filterable: true, + selectable: true, + filterRemote: true, + search: { + fields: ['path'] + }, + fieldName: fieldName, + toggleLabel: function(selected) { + if (selected.id == null) { + return selected.text; + } else { + return selected.kind + ": " + selected.path; + } + }, + data: function(term, dataCallback) { + return Api.namespaces(term, function(namespaces) { + var anyNamespace; + if (showAny) { + anyNamespace = { + text: 'Any namespace', + id: null + }; + namespaces.unshift(anyNamespace); + namespaces.splice(1, 0, 'divider'); + } + return dataCallback(namespaces); + }); + }, + text: function(namespace) { + if (namespace.id == null) { + return namespace.text; + } else { + return namespace.kind + ": " + namespace.path; + } + }, + renderRow: this.renderRow, + clicked: this.onSelectItem + }); + } + + NamespaceSelect.prototype.onSelectItem = function(item, el, e) { + return e.preventDefault(); + }; + + return NamespaceSelect; + + })(); + + this.NamespaceSelects = (function() { + function NamespaceSelects(opts) { + var ref; + if (opts == null) { + opts = {}; + } + this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-namespace-select'); + this.$dropdowns.each(function(i, dropdown) { + var $dropdown; + $dropdown = $(dropdown); + return new NamespaceSelect({ + dropdown: $dropdown + }); + }); + } + + return NamespaceSelects; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/namespace_select.js.coffee b/app/assets/javascripts/namespace_select.js.coffee deleted file mode 100644 index 3b419dff105..00000000000 --- a/app/assets/javascripts/namespace_select.js.coffee +++ /dev/null @@ -1,56 +0,0 @@ -class @NamespaceSelect - constructor: (opts) -> - { - @dropdown - } = opts - - showAny = true - fieldName = 'namespace_id' - - if @dropdown.attr 'data-field-name' - fieldName = @dropdown.data 'fieldName' - - if @dropdown.attr 'data-show-any' - showAny = @dropdown.data 'showAny' - - @dropdown.glDropdown( - filterable: true - selectable: true - filterRemote: true - search: - fields: ['path'] - fieldName: fieldName - toggleLabel: (selected) -> - return if not selected.id? then selected.text else "#{selected.kind}: #{selected.path}" - data: (term, dataCallback) -> - Api.namespaces term, (namespaces) -> - if showAny - anyNamespace = - text: 'Any namespace' - id: null - - namespaces.unshift(anyNamespace) - namespaces.splice 1, 0, 'divider' - - dataCallback(namespaces) - text: (namespace) -> - return if not namespace.id? then namespace.text else "#{namespace.kind}: #{namespace.path}" - renderRow: @renderRow - clicked: @onSelectItem - ) - - onSelectItem: (item, el, e) => - e.preventDefault() - -class @NamespaceSelects - constructor: (opts = {}) -> - { - @$dropdowns = $('.js-namespace-select') - } = opts - - @$dropdowns.each (i, dropdown) -> - $dropdown = $(dropdown) - - new NamespaceSelect( - dropdown: $dropdown - ) diff --git a/app/assets/javascripts/network/branch-graph.js b/app/assets/javascripts/network/branch-graph.js new file mode 100644 index 00000000000..c0fec1f8607 --- /dev/null +++ b/app/assets/javascripts/network/branch-graph.js @@ -0,0 +1,404 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.BranchGraph = (function() { + function BranchGraph(element1, options1) { + this.element = element1; + this.options = options1; + this.scrollTop = bind(this.scrollTop, this); + this.scrollBottom = bind(this.scrollBottom, this); + this.scrollRight = bind(this.scrollRight, this); + this.scrollLeft = bind(this.scrollLeft, this); + this.scrollUp = bind(this.scrollUp, this); + this.scrollDown = bind(this.scrollDown, this); + this.preparedCommits = {}; + this.mtime = 0; + this.mspace = 0; + this.parents = {}; + this.colors = ["#000"]; + this.offsetX = 150; + this.offsetY = 20; + this.unitTime = 30; + this.unitSpace = 10; + this.prev_start = -1; + this.load(); + } + + BranchGraph.prototype.load = function() { + return $.ajax({ + url: this.options.url, + method: "get", + dataType: "json", + success: $.proxy(function(data) { + $(".loading", this.element).hide(); + this.prepareData(data.days, data.commits); + return this.buildGraph(); + }, this) + }); + }; + + BranchGraph.prototype.prepareData = function(days, commits) { + var c, ch, cw, j, len, ref; + this.days = days; + this.commits = commits; + this.collectParents(); + this.graphHeight = $(this.element).height(); + this.graphWidth = $(this.element).width(); + ch = Math.max(this.graphHeight, this.offsetY + this.unitTime * this.mtime + 150); + cw = Math.max(this.graphWidth, this.offsetX + this.unitSpace * this.mspace + 300); + this.r = Raphael(this.element.get(0), cw, ch); + this.top = this.r.set(); + this.barHeight = Math.max(this.graphHeight, this.unitTime * this.days.length + 320); + ref = this.commits; + for (j = 0, len = ref.length; j < len; j++) { + c = ref[j]; + if (c.id in this.parents) { + c.isParent = true; + } + this.preparedCommits[c.id] = c; + this.markCommit(c); + } + return this.collectColors(); + }; + + BranchGraph.prototype.collectParents = function() { + var c, j, len, p, ref, results; + ref = this.commits; + results = []; + for (j = 0, len = ref.length; j < len; j++) { + c = ref[j]; + this.mtime = Math.max(this.mtime, c.time); + this.mspace = Math.max(this.mspace, c.space); + results.push((function() { + var l, len1, ref1, results1; + ref1 = c.parents; + results1 = []; + for (l = 0, len1 = ref1.length; l < len1; l++) { + p = ref1[l]; + this.parents[p[0]] = true; + results1.push(this.mspace = Math.max(this.mspace, p[1])); + } + return results1; + }).call(this)); + } + return results; + }; + + BranchGraph.prototype.collectColors = function() { + var k, results; + k = 0; + results = []; + while (k < this.mspace) { + this.colors.push(Raphael.getColor(.8)); + Raphael.getColor(); + Raphael.getColor(); + results.push(k++); + } + return results; + }; + + BranchGraph.prototype.buildGraph = function() { + var cuday, cumonth, day, j, len, mm, r, ref; + r = this.r; + cuday = 0; + cumonth = ""; + r.rect(0, 0, 40, this.barHeight).attr({ + fill: "#222" + }); + r.rect(40, 0, 30, this.barHeight).attr({ + fill: "#444" + }); + ref = this.days; + for (mm = j = 0, len = ref.length; j < len; mm = ++j) { + day = ref[mm]; + if (cuday !== day[0] || cumonth !== day[1]) { + r.text(55, this.offsetY + this.unitTime * mm, day[0]).attr({ + font: "12px Monaco, monospace", + fill: "#BBB" + }); + cuday = day[0]; + } + if (cumonth !== day[1]) { + r.text(20, this.offsetY + this.unitTime * mm, day[1]).attr({ + font: "12px Monaco, monospace", + fill: "#EEE" + }); + cumonth = day[1]; + } + } + this.renderPartialGraph(); + return this.bindEvents(); + }; + + BranchGraph.prototype.renderPartialGraph = function() { + var commit, end, i, isGraphEdge, start, x, y; + start = Math.floor((this.element.scrollTop() - this.offsetY) / this.unitTime) - 10; + if (start < 0) { + isGraphEdge = true; + start = 0; + } + end = start + 40; + if (this.commits.length < end) { + isGraphEdge = true; + end = this.commits.length; + } + if (this.prev_start === -1 || Math.abs(this.prev_start - start) > 10 || isGraphEdge) { + i = start; + this.prev_start = start; + while (i < end) { + commit = this.commits[i]; + i += 1; + if (commit.hasDrawn !== true) { + x = this.offsetX + this.unitSpace * (this.mspace - commit.space); + y = this.offsetY + this.unitTime * commit.time; + this.drawDot(x, y, commit); + this.drawLines(x, y, commit); + this.appendLabel(x, y, commit); + this.appendAnchor(x, y, commit); + commit.hasDrawn = true; + } + } + return this.top.toFront(); + } + }; + + BranchGraph.prototype.bindEvents = function() { + var element; + element = this.element; + return $(element).scroll((function(_this) { + return function(event) { + return _this.renderPartialGraph(); + }; + })(this)); + }; + + BranchGraph.prototype.scrollDown = function() { + this.element.scrollTop(this.element.scrollTop() + 50); + return this.renderPartialGraph(); + }; + + BranchGraph.prototype.scrollUp = function() { + this.element.scrollTop(this.element.scrollTop() - 50); + return this.renderPartialGraph(); + }; + + BranchGraph.prototype.scrollLeft = function() { + this.element.scrollLeft(this.element.scrollLeft() - 50); + return this.renderPartialGraph(); + }; + + BranchGraph.prototype.scrollRight = function() { + this.element.scrollLeft(this.element.scrollLeft() + 50); + return this.renderPartialGraph(); + }; + + BranchGraph.prototype.scrollBottom = function() { + return this.element.scrollTop(this.element.find('svg').height()); + }; + + BranchGraph.prototype.scrollTop = function() { + return this.element.scrollTop(0); + }; + + BranchGraph.prototype.appendLabel = function(x, y, commit) { + var label, r, rect, shortrefs, text, textbox, triangle; + if (!commit.refs) { + return; + } + r = this.r; + shortrefs = commit.refs; + if (shortrefs.length > 17) { + shortrefs = shortrefs.substr(0, 15) + "…"; + } + text = r.text(x + 4, y, shortrefs).attr({ + "text-anchor": "start", + font: "10px Monaco, monospace", + fill: "#FFF", + title: commit.refs + }); + textbox = text.getBBox(); + rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr({ + fill: "#000", + "fill-opacity": .5, + stroke: "none" + }); + triangle = r.path(["M", x - 5, y, "L", x - 15, y - 4, "L", x - 15, y + 4, "Z"]).attr({ + fill: "#000", + "fill-opacity": .5, + stroke: "none" + }); + label = r.set(rect, text); + label.transform(["t", -rect.getBBox().width - 15, 0]); + return text.toFront(); + }; + + BranchGraph.prototype.appendAnchor = function(x, y, commit) { + var anchor, options, r, top; + r = this.r; + top = this.top; + options = this.options; + anchor = r.circle(x, y, 10).attr({ + fill: "#000", + opacity: 0, + cursor: "pointer" + }).click(function() { + return window.open(options.commit_url.replace("%s", commit.id), "_blank"); + }).hover(function() { + this.tooltip = r.commitTooltip(x + 5, y, commit); + return top.push(this.tooltip.insertBefore(this)); + }, function() { + return this.tooltip && this.tooltip.remove() && delete this.tooltip; + }); + return top.push(anchor); + }; + + BranchGraph.prototype.drawDot = function(x, y, commit) { + var avatar_box_x, avatar_box_y, r; + r = this.r; + r.circle(x, y, 3).attr({ + fill: this.colors[commit.space], + stroke: "none" + }); + avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10; + avatar_box_y = y - 10; + r.rect(avatar_box_x, avatar_box_y, 20, 20).attr({ + stroke: this.colors[commit.space], + "stroke-width": 2 + }); + r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20); + return r.text(this.offsetX + this.unitSpace * this.mspace + 35, y, commit.message.split("\n")[0]).attr({ + "text-anchor": "start", + font: "14px Monaco, monospace" + }); + }; + + BranchGraph.prototype.drawLines = function(x, y, commit) { + var arrow, color, i, j, len, offset, parent, parentCommit, parentX1, parentX2, parentY, r, ref, results, route; + r = this.r; + ref = commit.parents; + results = []; + for (i = j = 0, len = ref.length; j < len; i = ++j) { + parent = ref[i]; + parentCommit = this.preparedCommits[parent[0]]; + parentY = this.offsetY + this.unitTime * parentCommit.time; + parentX1 = this.offsetX + this.unitSpace * (this.mspace - parentCommit.space); + parentX2 = this.offsetX + this.unitSpace * (this.mspace - parent[1]); + if (parentCommit.space <= commit.space) { + color = this.colors[commit.space]; + } else { + color = this.colors[parentCommit.space]; + } + if (parent[1] === commit.space) { + offset = [0, 5]; + arrow = "l-2,5,4,0,-2,-5,0,5"; + } else if (parent[1] < commit.space) { + offset = [3, 3]; + arrow = "l5,0,-2,4,-3,-4,4,2"; + } else { + offset = [-3, 3]; + arrow = "l-5,0,2,4,3,-4,-4,2"; + } + route = ["M", x + offset[0], y + offset[1]]; + if (i > 0) { + route.push(arrow); + } + if (commit.space !== parentCommit.space || commit.space !== parent[1]) { + route.push("L", parentX2, y + 10, "L", parentX2, parentY - 5); + } + route.push("L", parentX1, parentY); + results.push(r.path(route).attr({ + stroke: color, + "stroke-width": 2 + })); + } + return results; + }; + + BranchGraph.prototype.markCommit = function(commit) { + var r, x, y; + if (commit.id === this.options.commit_id) { + r = this.r; + x = this.offsetX + this.unitSpace * (this.mspace - commit.space); + y = this.offsetY + this.unitTime * commit.time; + r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr({ + fill: "#000", + "fill-opacity": .5, + stroke: "none" + }); + return this.element.scrollTop(y - this.graphHeight / 2); + } + }; + + return BranchGraph; + + })(); + + Raphael.prototype.commitTooltip = function(x, y, commit) { + var boxHeight, boxWidth, icon, idText, messageText, nameText, rect, textSet, tooltip; + boxWidth = 300; + boxHeight = 200; + icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20); + nameText = this.text(x + 25, y + 10, commit.author.name); + idText = this.text(x, y + 35, commit.id); + messageText = this.text(x, y + 50, commit.message); + textSet = this.set(icon, nameText, idText, messageText).attr({ + "text-anchor": "start", + font: "12px Monaco, monospace" + }); + nameText.attr({ + font: "14px Arial", + "font-weight": "bold" + }); + idText.attr({ + fill: "#AAA" + }); + this.textWrap(messageText, boxWidth - 50); + rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({ + fill: "#FFF", + stroke: "#000", + "stroke-linecap": "round", + "stroke-width": 2 + }); + tooltip = this.set(rect, textSet); + rect.attr({ + height: tooltip.getBBox().height + 10, + width: tooltip.getBBox().width + 10 + }); + tooltip.transform(["t", 20, 20]); + return tooltip; + }; + + Raphael.prototype.textWrap = function(t, width) { + var abc, b, content, h, j, len, letterWidth, s, word, words, x; + content = t.attr("text"); + abc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + t.attr({ + text: abc + }); + letterWidth = t.getBBox().width / abc.length; + t.attr({ + text: content + }); + words = content.split(" "); + x = 0; + s = []; + for (j = 0, len = words.length; j < len; j++) { + word = words[j]; + if (x + (word.length * letterWidth) > width) { + s.push("\n"); + x = 0; + } + x += word.length * letterWidth; + s.push(word + " "); + } + t.attr({ + text: s.join("") + }); + b = t.getBBox(); + h = Math.abs(b.y2) - Math.abs(b.y) + 1; + return t.attr({ + y: b.y + h + }); + }; + +}).call(this); diff --git a/app/assets/javascripts/network/branch-graph.js.coffee b/app/assets/javascripts/network/branch-graph.js.coffee deleted file mode 100644 index f2fd2a775a4..00000000000 --- a/app/assets/javascripts/network/branch-graph.js.coffee +++ /dev/null @@ -1,340 +0,0 @@ -class @BranchGraph - constructor: (@element, @options) -> - @preparedCommits = {} - @mtime = 0 - @mspace = 0 - @parents = {} - @colors = ["#000"] - @offsetX = 150 - @offsetY = 20 - @unitTime = 30 - @unitSpace = 10 - @prev_start = -1 - @load() - - load: -> - $.ajax - url: @options.url - method: "get" - dataType: "json" - success: $.proxy((data) -> - $(".loading", @element).hide() - @prepareData data.days, data.commits - @buildGraph() - , this) - - prepareData: (@days, @commits) -> - @collectParents() - @graphHeight = $(@element).height() - @graphWidth = $(@element).width() - ch = Math.max(@graphHeight, @offsetY + @unitTime * @mtime + 150) - cw = Math.max(@graphWidth, @offsetX + @unitSpace * @mspace + 300) - @r = Raphael(@element.get(0), cw, ch) - @top = @r.set() - @barHeight = Math.max(@graphHeight, @unitTime * @days.length + 320) - - for c in @commits - c.isParent = true if c.id of @parents - @preparedCommits[c.id] = c - @markCommit(c) - - @collectColors() - - collectParents: -> - for c in @commits - @mtime = Math.max(@mtime, c.time) - @mspace = Math.max(@mspace, c.space) - for p in c.parents - @parents[p[0]] = true - @mspace = Math.max(@mspace, p[1]) - - collectColors: -> - k = 0 - while k < @mspace - @colors.push Raphael.getColor(.8) - # Skipping a few colors in the spectrum to get more contrast between colors - Raphael.getColor() - Raphael.getColor() - k++ - - buildGraph: -> - r = @r - cuday = 0 - cumonth = "" - - r.rect(0, 0, 40, @barHeight).attr fill: "#222" - r.rect(40, 0, 30, @barHeight).attr fill: "#444" - - for day, mm in @days - if cuday isnt day[0] || cumonth isnt day[1] - # Dates - r.text(55, @offsetY + @unitTime * mm, day[0]) - .attr( - font: "12px Monaco, monospace" - fill: "#BBB" - ) - cuday = day[0] - - if cumonth isnt day[1] - # Months - r.text(20, @offsetY + @unitTime * mm, day[1]) - .attr( - font: "12px Monaco, monospace" - fill: "#EEE" - ) - cumonth = day[1] - - @renderPartialGraph() - - @bindEvents() - - renderPartialGraph: -> - start = Math.floor((@element.scrollTop() - @offsetY) / @unitTime) - 10 - if start < 0 - isGraphEdge = true - start = 0 - end = start + 40 - if @commits.length < end - isGraphEdge = true - end = @commits.length - - if @prev_start == -1 or Math.abs(@prev_start - start) > 10 or isGraphEdge - i = start - - @prev_start = start - - while i < end - commit = @commits[i] - i += 1 - - if commit.hasDrawn isnt true - x = @offsetX + @unitSpace * (@mspace - commit.space) - y = @offsetY + @unitTime * commit.time - - @drawDot(x, y, commit) - - @drawLines(x, y, commit) - - @appendLabel(x, y, commit) - - @appendAnchor(x, y, commit) - - commit.hasDrawn = true - - @top.toFront() - - bindEvents: -> - element = @element - - $(element).scroll (event) => - @renderPartialGraph() - - scrollDown: => - @element.scrollTop @element.scrollTop() + 50 - @renderPartialGraph() - - scrollUp: => - @element.scrollTop @element.scrollTop() - 50 - @renderPartialGraph() - - scrollLeft: => - @element.scrollLeft @element.scrollLeft() - 50 - @renderPartialGraph() - - scrollRight: => - @element.scrollLeft @element.scrollLeft() + 50 - @renderPartialGraph() - - scrollBottom: => - @element.scrollTop @element.find('svg').height() - - scrollTop: => - @element.scrollTop 0 - - appendLabel: (x, y, commit) -> - return unless commit.refs - - r = @r - shortrefs = commit.refs - # Truncate if longer than 15 chars - shortrefs = shortrefs.substr(0, 15) + "…" if shortrefs.length > 17 - text = r.text(x + 4, y, shortrefs).attr( - "text-anchor": "start" - font: "10px Monaco, monospace" - fill: "#FFF" - title: commit.refs - ) - textbox = text.getBBox() - # Create rectangle based on the size of the textbox - rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr( - fill: "#000" - "fill-opacity": .5 - stroke: "none" - ) - triangle = r.path(["M", x - 5, y, "L", x - 15, y - 4, "L", x - 15, y + 4, "Z"]).attr( - fill: "#000" - "fill-opacity": .5 - stroke: "none" - ) - - label = r.set(rect, text) - label.transform(["t", -rect.getBBox().width - 15, 0]) - - # Set text to front - text.toFront() - - appendAnchor: (x, y, commit) -> - r = @r - top = @top - options = @options - anchor = r.circle(x, y, 10).attr( - fill: "#000" - opacity: 0 - cursor: "pointer" - ).click(-> - window.open options.commit_url.replace("%s", commit.id), "_blank" - ).hover(-> - @tooltip = r.commitTooltip(x + 5, y, commit) - top.push @tooltip.insertBefore(this) - , -> - @tooltip and @tooltip.remove() and delete @tooltip - ) - top.push anchor - - drawDot: (x, y, commit) -> - r = @r - r.circle(x, y, 3).attr( - fill: @colors[commit.space] - stroke: "none" - ) - - avatar_box_x = @offsetX + @unitSpace * @mspace + 10 - avatar_box_y = y - 10 - r.rect(avatar_box_x, avatar_box_y, 20, 20).attr( - stroke: @colors[commit.space] - "stroke-width": 2 - ) - r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20) - r.text(@offsetX + @unitSpace * @mspace + 35, y, commit.message.split("\n")[0]).attr( - "text-anchor": "start" - font: "14px Monaco, monospace" - ) - - drawLines: (x, y, commit) -> - r = @r - for parent, i in commit.parents - parentCommit = @preparedCommits[parent[0]] - parentY = @offsetY + @unitTime * parentCommit.time - parentX1 = @offsetX + @unitSpace * (@mspace - parentCommit.space) - parentX2 = @offsetX + @unitSpace * (@mspace - parent[1]) - - # Set line color - if parentCommit.space <= commit.space - color = @colors[commit.space] - - else - color = @colors[parentCommit.space] - - # Build line shape - if parent[1] is commit.space - offset = [0, 5] - arrow = "l-2,5,4,0,-2,-5,0,5" - - else if parent[1] < commit.space - offset = [3, 3] - arrow = "l5,0,-2,4,-3,-4,4,2" - - else - offset = [-3, 3] - arrow = "l-5,0,2,4,3,-4,-4,2" - - # Start point - route = ["M", x + offset[0], y + offset[1]] - - # Add arrow if not first parent - if i > 0 - route.push(arrow) - - # Circumvent if overlap - if commit.space isnt parentCommit.space or commit.space isnt parent[1] - route.push( - "L", parentX2, y + 10, - "L", parentX2, parentY - 5, - ) - - # End point - route.push("L", parentX1, parentY) - - r - .path(route) - .attr( - stroke: color - "stroke-width": 2) - - markCommit: (commit) -> - if commit.id is @options.commit_id - r = @r - x = @offsetX + @unitSpace * (@mspace - commit.space) - y = @offsetY + @unitTime * commit.time - r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr( - fill: "#000" - "fill-opacity": .5 - stroke: "none" - ) - # Displayed in the center - @element.scrollTop(y - @graphHeight / 2) - -Raphael::commitTooltip = (x, y, commit) -> - boxWidth = 300 - boxHeight = 200 - icon = @image(gon.relative_url_root + commit.author.icon, x, y, 20, 20) - nameText = @text(x + 25, y + 10, commit.author.name) - idText = @text(x, y + 35, commit.id) - messageText = @text(x, y + 50, commit.message) - textSet = @set(icon, nameText, idText, messageText).attr( - "text-anchor": "start" - font: "12px Monaco, monospace" - ) - nameText.attr( - font: "14px Arial" - "font-weight": "bold" - ) - - idText.attr fill: "#AAA" - @textWrap messageText, boxWidth - 50 - rect = @rect(x - 10, y - 10, boxWidth, 100, 4).attr( - fill: "#FFF" - stroke: "#000" - "stroke-linecap": "round" - "stroke-width": 2 - ) - tooltip = @set(rect, textSet) - rect.attr( - height: tooltip.getBBox().height + 10 - width: tooltip.getBBox().width + 10 - ) - - tooltip.transform ["t", 20, 20] - tooltip - -Raphael::textWrap = (t, width) -> - content = t.attr("text") - abc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - t.attr text: abc - letterWidth = t.getBBox().width / abc.length - t.attr text: content - words = content.split(" ") - x = 0 - s = [] - - for word in words - if x + (word.length * letterWidth) > width - s.push "\n" - x = 0 - x += word.length * letterWidth - s.push word + " " - - t.attr text: s.join("") - b = t.getBBox() - h = Math.abs(b.y2) - Math.abs(b.y) + 1 - t.attr y: b.y + h diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js new file mode 100644 index 00000000000..7baebcd100a --- /dev/null +++ b/app/assets/javascripts/network/network.js @@ -0,0 +1,19 @@ +(function() { + this.Network = (function() { + function Network(opts) { + var vph; + $("#filter_ref").click(function() { + return $(this).closest('form').submit(); + }); + this.branch_graph = new BranchGraph($(".network-graph"), opts); + vph = $(window).height() - 250; + $('.network-graph').css({ + 'height': vph + 'px' + }); + } + + return Network; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/network/network.js.coffee b/app/assets/javascripts/network/network.js.coffee deleted file mode 100644 index f4ef07a50a7..00000000000 --- a/app/assets/javascripts/network/network.js.coffee +++ /dev/null @@ -1,9 +0,0 @@ -class @Network - constructor: (opts) -> - $("#filter_ref").click -> - $(this).closest('form').submit() - - @branch_graph = new BranchGraph($(".network-graph"), opts) - - vph = $(window).height() - 250 - $('.network-graph').css 'height': (vph + 'px') diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js new file mode 100644 index 00000000000..6a7422a7755 --- /dev/null +++ b/app/assets/javascripts/network/network_bundle.js @@ -0,0 +1,16 @@ + +/*= require_tree . */ + +(function() { + $(function() { + var network_graph; + network_graph = new Network({ + url: $(".network-graph").attr('data-url'), + commit_url: $(".network-graph").attr('data-commit-url'), + ref: $(".network-graph").attr('data-ref'), + commit_id: $(".network-graph").attr('data-commit-id') + }); + return new ShortcutsNetwork(network_graph.branch_graph); + }); + +}).call(this); diff --git a/app/assets/javascripts/network/network_bundle.js.coffee b/app/assets/javascripts/network/network_bundle.js.coffee deleted file mode 100644 index f75f63869c5..00000000000 --- a/app/assets/javascripts/network/network_bundle.js.coffee +++ /dev/null @@ -1,17 +0,0 @@ -# This is a manifest file that'll be compiled into including all the files listed below. -# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically -# be included in the compiled file accessible from http://example.com/assets/application.js -# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -# the compiled file. -# -#= require_tree . - -$ -> - network_graph = new Network({ - url: $(".network-graph").attr('data-url'), - commit_url: $(".network-graph").attr('data-commit-url'), - ref: $(".network-graph").attr('data-ref'), - commit_id: $(".network-graph").attr('data-commit-id') - }) - - new ShortcutsNetwork(network_graph.branch_graph) diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js new file mode 100644 index 00000000000..20aa2fced27 --- /dev/null +++ b/app/assets/javascripts/new_branch_form.js @@ -0,0 +1,104 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + + this.NewBranchForm = (function() { + function NewBranchForm(form, availableRefs) { + this.validate = bind(this.validate, this); + this.branchNameError = form.find('.js-branch-name-error'); + this.name = form.find('.js-branch-name'); + this.ref = form.find('#ref'); + this.setupAvailableRefs(availableRefs); + this.setupRestrictions(); + this.addBinding(); + this.init(); + } + + NewBranchForm.prototype.addBinding = function() { + return this.name.on('blur', this.validate); + }; + + NewBranchForm.prototype.init = function() { + if (this.name.val().length > 0) { + return this.name.trigger('blur'); + } + }; + + NewBranchForm.prototype.setupAvailableRefs = function(availableRefs) { + return this.ref.autocomplete({ + source: availableRefs, + minLength: 1 + }); + }; + + NewBranchForm.prototype.setupRestrictions = function() { + var endsWith, invalid, single, startsWith; + startsWith = { + pattern: /^(\/|\.)/g, + prefix: "can't start with", + conjunction: "or" + }; + endsWith = { + pattern: /(\/|\.|\.lock)$/g, + prefix: "can't end in", + conjunction: "or" + }; + invalid = { + pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g, + prefix: "can't contain", + conjunction: ", " + }; + single = { + pattern: /^@+$/g, + prefix: "can't be", + conjunction: "or" + }; + return this.restrictions = [startsWith, invalid, endsWith, single]; + }; + + NewBranchForm.prototype.validate = function() { + var errorMessage, errors, formatter, unique, validator; + this.branchNameError.empty(); + unique = function(values, value) { + if (indexOf.call(values, value) < 0) { + values.push(value); + } + return values; + }; + formatter = function(values, restriction) { + var formatted; + formatted = values.map(function(value) { + switch (false) { + case !/\s/.test(value): + return 'spaces'; + case !/\/{2,}/g.test(value): + return 'consecutive slashes'; + default: + return "'" + value + "'"; + } + }); + return restriction.prefix + " " + (formatted.join(restriction.conjunction)); + }; + validator = (function(_this) { + return function(errors, restriction) { + var matched; + matched = _this.name.val().match(restriction.pattern); + if (matched) { + return errors.concat(formatter(matched.reduce(unique, []), restriction)); + } else { + return errors; + } + }; + })(this); + errors = this.restrictions.reduce(validator, []); + if (errors.length > 0) { + errorMessage = $("<span/>").text(errors.join(', ')); + return this.branchNameError.append(errorMessage); + } + }; + + return NewBranchForm; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/new_branch_form.js.coffee b/app/assets/javascripts/new_branch_form.js.coffee deleted file mode 100644 index 4b350854f78..00000000000 --- a/app/assets/javascripts/new_branch_form.js.coffee +++ /dev/null @@ -1,78 +0,0 @@ -class @NewBranchForm - constructor: (form, availableRefs) -> - @branchNameError = form.find('.js-branch-name-error') - @name = form.find('.js-branch-name') - @ref = form.find('#ref') - - @setupAvailableRefs(availableRefs) - @setupRestrictions() - @addBinding() - @init() - - addBinding: -> - @name.on 'blur', @validate - - init: -> - @name.trigger 'blur' if @name.val().length > 0 - - setupAvailableRefs: (availableRefs) -> - @ref.autocomplete - source: availableRefs, - minLength: 1 - - setupRestrictions: -> - startsWith = { - pattern: /^(\/|\.)/g, - prefix: "can't start with", - conjunction: "or" - } - - endsWith = { - pattern: /(\/|\.|\.lock)$/g, - prefix: "can't end in", - conjunction: "or" - } - - invalid = { - pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g - prefix: "can't contain", - conjunction: ", " - } - - single = { - pattern: /^@+$/g - prefix: "can't be", - conjunction: "or" - } - - @restrictions = [startsWith, invalid, endsWith, single] - - validate: => - @branchNameError.empty() - - unique = (values, value) -> - values.push(value) unless value in values - values - - formatter = (values, restriction) -> - formatted = values.map (value) -> - switch - when /\s/.test value then 'spaces' - when /\/{2,}/g.test value then 'consecutive slashes' - else "'#{value}'" - - "#{restriction.prefix} #{formatted.join(restriction.conjunction)}" - - validator = (errors, restriction) => - matched = @name.val().match(restriction.pattern) - - if matched - errors.concat formatter(matched.reduce(unique, []), restriction) - else - errors - - errors = @restrictions.reduce validator, [] - - if errors.length > 0 - errorMessage = $("<span/>").text(errors.join(', ')) - @branchNameError.append(errorMessage) diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js new file mode 100644 index 00000000000..21bf8867f7b --- /dev/null +++ b/app/assets/javascripts/new_commit_form.js @@ -0,0 +1,34 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.NewCommitForm = (function() { + function NewCommitForm(form) { + this.renderDestination = bind(this.renderDestination, this); + this.newBranch = form.find('.js-target-branch'); + this.originalBranch = form.find('.js-original-branch'); + this.createMergeRequest = form.find('.js-create-merge-request'); + this.createMergeRequestContainer = form.find('.js-create-merge-request-container'); + this.renderDestination(); + this.newBranch.keyup(this.renderDestination); + } + + NewCommitForm.prototype.renderDestination = function() { + var different; + different = this.newBranch.val() !== this.originalBranch.val(); + if (different) { + this.createMergeRequestContainer.show(); + if (!this.wasDifferent) { + this.createMergeRequest.prop('checked', true); + } + } else { + this.createMergeRequestContainer.hide(); + this.createMergeRequest.prop('checked', false); + } + return this.wasDifferent = different; + }; + + return NewCommitForm; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/new_commit_form.js.coffee b/app/assets/javascripts/new_commit_form.js.coffee deleted file mode 100644 index 03f0f51acfa..00000000000 --- a/app/assets/javascripts/new_commit_form.js.coffee +++ /dev/null @@ -1,21 +0,0 @@ -class @NewCommitForm - constructor: (form) -> - @newBranch = form.find('.js-target-branch') - @originalBranch = form.find('.js-original-branch') - @createMergeRequest = form.find('.js-create-merge-request') - @createMergeRequestContainer = form.find('.js-create-merge-request-container') - - @renderDestination() - @newBranch.keyup @renderDestination - - renderDestination: => - different = @newBranch.val() != @originalBranch.val() - - if different - @createMergeRequestContainer.show() - @createMergeRequest.prop('checked', true) unless @wasDifferent - else - @createMergeRequestContainer.hide() - @createMergeRequest.prop('checked', false) - - @wasDifferent = different diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js new file mode 100644 index 00000000000..9ece474d994 --- /dev/null +++ b/app/assets/javascripts/notes.js @@ -0,0 +1,732 @@ + +/*= require autosave */ + + +/*= require autosize */ + + +/*= require dropzone */ + + +/*= require dropzone_input */ + + +/*= require gfm_auto_complete */ + + +/*= require jquery.atwho */ + + +/*= require task_list */ + +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Notes = (function() { + var isMetaKey; + + Notes.interval = null; + + function Notes(notes_url, note_ids, last_fetched_at, view) { + this.updateTargetButtons = bind(this.updateTargetButtons, this); + this.updateCloseButton = bind(this.updateCloseButton, this); + this.visibilityChange = bind(this.visibilityChange, this); + this.cancelDiscussionForm = bind(this.cancelDiscussionForm, this); + this.addDiffNote = bind(this.addDiffNote, this); + this.setupDiscussionNoteForm = bind(this.setupDiscussionNoteForm, this); + this.replyToDiscussionNote = bind(this.replyToDiscussionNote, this); + this.removeNote = bind(this.removeNote, this); + this.cancelEdit = bind(this.cancelEdit, this); + this.updateNote = bind(this.updateNote, this); + this.addDiscussionNote = bind(this.addDiscussionNote, this); + this.addNoteError = bind(this.addNoteError, this); + this.addNote = bind(this.addNote, this); + this.resetMainTargetForm = bind(this.resetMainTargetForm, this); + this.refresh = bind(this.refresh, this); + this.keydownNoteText = bind(this.keydownNoteText, this); + this.notes_url = notes_url; + this.note_ids = note_ids; + this.last_fetched_at = last_fetched_at; + this.view = view; + this.noteable_url = document.URL; + this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge")); + this.basePollingInterval = 15000; + this.maxPollingSteps = 4; + this.cleanBinding(); + this.addBinding(); + this.setPollingInterval(); + this.setupMainTargetNoteForm(); + this.initTaskList(); + } + + Notes.prototype.addBinding = function() { + $(document).on("ajax:success", ".js-main-target-form", this.addNote); + $(document).on("ajax:success", ".js-discussion-note-form", this.addDiscussionNote); + $(document).on("ajax:error", ".js-main-target-form", this.addNoteError); + $(document).on("ajax:success", "form.edit-note", this.updateNote); + $(document).on("click", ".js-note-edit", this.showEditForm); + $(document).on("click", ".note-edit-cancel", this.cancelEdit); + $(document).on("click", ".js-comment-button", this.updateCloseButton); + $(document).on("keyup input", ".js-note-text", this.updateTargetButtons); + $(document).on("click", ".js-note-delete", this.removeNote); + $(document).on("click", ".js-note-attachment-delete", this.removeAttachment); + $(document).on("ajax:complete", ".js-main-target-form", this.reenableTargetFormSubmitButton); + $(document).on("ajax:success", ".js-main-target-form", this.resetMainTargetForm); + $(document).on("click", ".js-note-discard", this.resetMainTargetForm); + $(document).on("change", ".js-note-attachment-input", this.updateFormAttachment); + $(document).on("click", ".js-discussion-reply-button", this.replyToDiscussionNote); + $(document).on("click", ".js-add-diff-note-button", this.addDiffNote); + $(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm); + $(document).on("visibilitychange", this.visibilityChange); + $(document).on("issuable:change", this.refresh); + return $(document).on("keydown", ".js-note-text", this.keydownNoteText); + }; + + Notes.prototype.cleanBinding = function() { + $(document).off("ajax:success", ".js-main-target-form"); + $(document).off("ajax:success", ".js-discussion-note-form"); + $(document).off("ajax:success", "form.edit-note"); + $(document).off("click", ".js-note-edit"); + $(document).off("click", ".note-edit-cancel"); + $(document).off("click", ".js-note-delete"); + $(document).off("click", ".js-note-attachment-delete"); + $(document).off("ajax:complete", ".js-main-target-form"); + $(document).off("ajax:success", ".js-main-target-form"); + $(document).off("click", ".js-discussion-reply-button"); + $(document).off("click", ".js-add-diff-note-button"); + $(document).off("visibilitychange"); + $(document).off("keyup", ".js-note-text"); + $(document).off("click", ".js-note-target-reopen"); + $(document).off("click", ".js-note-target-close"); + $(document).off("click", ".js-note-discard"); + $(document).off("keydown", ".js-note-text"); + $('.note .js-task-list-container').taskList('disable'); + return $(document).off('tasklist:changed', '.note .js-task-list-container'); + }; + + Notes.prototype.keydownNoteText = function(e) { + var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText; + if (isMetaKey(e)) { + return; + } + $textarea = $(e.target); + switch (e.which) { + case 38: + if ($textarea.val() !== '') { + return; + } + myLastNote = $("li.note[data-author-id='" + gon.current_user_id + "'][data-editable]:last"); + if (myLastNote.length) { + myLastNoteEditBtn = myLastNote.find('.js-note-edit'); + return myLastNoteEditBtn.trigger('click', [true, myLastNote]); + } + break; + case 27: + discussionNoteForm = $textarea.closest('.js-discussion-note-form'); + if (discussionNoteForm.length) { + if ($textarea.val() !== '') { + if (!confirm('Are you sure you want to cancel creating this comment?')) { + return; + } + } + this.removeDiscussionNoteForm(discussionNoteForm); + return; + } + editNote = $textarea.closest('.note'); + if (editNote.length) { + originalText = $textarea.closest('form').data('original-note'); + newText = $textarea.val(); + if (originalText !== newText) { + if (!confirm('Are you sure you want to cancel editing this comment?')) { + return; + } + } + return this.removeNoteEditForm(editNote); + } + } + }; + + isMetaKey = function(e) { + return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; + }; + + Notes.prototype.initRefresh = function() { + clearInterval(Notes.interval); + return Notes.interval = setInterval((function(_this) { + return function() { + return _this.refresh(); + }; + })(this), this.pollingInterval); + }; + + Notes.prototype.refresh = function() { + if (!document.hidden && document.URL.indexOf(this.noteable_url) === 0) { + return this.getContent(); + } + }; + + Notes.prototype.getContent = function() { + if (this.refreshing) { + return; + } + this.refreshing = true; + return $.ajax({ + url: this.notes_url, + data: "last_fetched_at=" + this.last_fetched_at, + dataType: "json", + success: (function(_this) { + return function(data) { + var notes; + notes = data.notes; + _this.last_fetched_at = data.last_fetched_at; + _this.setPollingInterval(data.notes.length); + return $.each(notes, function(i, note) { + if (note.discussion_html != null) { + return _this.renderDiscussionNote(note); + } else { + return _this.renderNote(note); + } + }); + }; + })(this) + }).always((function(_this) { + return function() { + return _this.refreshing = false; + }; + })(this)); + }; + + + /* + Increase @pollingInterval up to 120 seconds on every function call, + if `shouldReset` has a truthy value, 'null' or 'undefined' the variable + will reset to @basePollingInterval. + + Note: this function is used to gradually increase the polling interval + if there aren't new notes coming from the server + */ + + Notes.prototype.setPollingInterval = function(shouldReset) { + var nthInterval; + if (shouldReset == null) { + shouldReset = true; + } + nthInterval = this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1); + if (shouldReset) { + this.pollingInterval = this.basePollingInterval; + } else if (this.pollingInterval < nthInterval) { + this.pollingInterval *= 2; + } + return this.initRefresh(); + }; + + + /* + Render note in main comments area. + + Note: for rendering inline notes use renderDiscussionNote + */ + + Notes.prototype.renderNote = function(note) { + var $notesList, votesBlock; + if (!note.valid) { + if (note.award) { + new Flash('You have already awarded this emoji!', 'alert'); + } + return; + } + if (note.award) { + votesBlock = $('.js-awards-block').eq(0); + gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.name); + return gl.awardsHandler.scrollToAwards(); + } else if (this.isNewNote(note)) { + this.note_ids.push(note.id); + $notesList = $('ul.main-notes-list'); + $notesList.append(note.html).syntaxHighlight(); + gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false); + this.initTaskList(); + return this.updateNotesCount(1); + } + }; + + + /* + Check if note does not exists on page + */ + + Notes.prototype.isNewNote = function(note) { + return $.inArray(note.id, this.note_ids) === -1; + }; + + Notes.prototype.isParallelView = function() { + return this.view === 'parallel'; + }; + + + /* + Render note in discussion area. + + Note: for rendering inline notes use renderDiscussionNote + */ + + Notes.prototype.renderDiscussionNote = function(note) { + var discussionContainer, form, note_html, row; + if (!this.isNewNote(note)) { + return; + } + this.note_ids.push(note.id); + form = $("#new-discussion-note-form-" + note.discussion_id); + if ((note.original_discussion_id != null) && form.length === 0) { + form = $("#new-discussion-note-form-" + note.original_discussion_id); + } + row = form.closest("tr"); + note_html = $(note.html); + note_html.syntaxHighlight(); + discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); + if ((note.original_discussion_id != null) && discussionContainer.length === 0) { + discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']"); + } + if (discussionContainer.length === 0) { + row.after(note.diff_discussion_html); + row.next().find(".note").remove(); + discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); + discussionContainer.append(note_html); + if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) { + $('ul.main-notes-list').append(note.discussion_html).syntaxHighlight(); + } + } else { + discussionContainer.append(note_html); + } + gl.utils.localTimeAgo($('.js-timeago', note_html), false); + return this.updateNotesCount(1); + }; + + + /* + Called in response the main target form has been successfully submitted. + + Removes any errors. + Resets text and preview. + Resets buttons. + */ + + Notes.prototype.resetMainTargetForm = function(e) { + var form; + form = $(".js-main-target-form"); + form.find(".js-errors").remove(); + form.find(".js-md-write-button").click(); + form.find(".js-note-text").val("").trigger("input"); + form.find(".js-note-text").data("autosave").reset(); + return this.updateTargetButtons(e); + }; + + Notes.prototype.reenableTargetFormSubmitButton = function() { + var form; + form = $(".js-main-target-form"); + return form.find(".js-note-text").trigger("input"); + }; + + + /* + Shows the main form and does some setup on it. + + Sets some hidden fields in the form. + */ + + Notes.prototype.setupMainTargetNoteForm = function() { + var form; + form = $(".js-new-note-form"); + this.formClone = form.clone(); + this.setupNoteForm(form); + form.removeClass("js-new-note-form"); + form.addClass("js-main-target-form"); + form.find("#note_line_code").remove(); + form.find("#note_position").remove(); + form.find("#note_type").remove(); + return this.parentTimeline = form.parents('.timeline'); + }; + + + /* + General note form setup. + + deactivates the submit button when text is empty + hides the preview button when text is empty + setup GFM auto complete + show the form + */ + + Notes.prototype.setupNoteForm = function(form) { + var textarea; + new GLForm(form); + textarea = form.find(".js-note-text"); + return new Autosave(textarea, ["Note", form.find("#note_noteable_type").val(), form.find("#note_noteable_id").val(), form.find("#note_commit_id").val(), form.find("#note_type").val(), form.find("#note_line_code").val(), form.find("#note_position").val()]); + }; + + + /* + Called in response to the new note form being submitted + + Adds new note to list. + */ + + Notes.prototype.addNote = function(xhr, note, status) { + return this.renderNote(note); + }; + + Notes.prototype.addNoteError = function(xhr, note, status) { + return new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', this.parentTimeline); + }; + + + /* + Called in response to the new note form being submitted + + Adds new note to list. + */ + + Notes.prototype.addDiscussionNote = function(xhr, note, status) { + this.renderDiscussionNote(note); + return this.removeDiscussionNoteForm($(xhr.target)); + }; + + + /* + Called in response to the edit note form being submitted + + Updates the current note field. + */ + + Notes.prototype.updateNote = function(_xhr, note, _status) { + var $html, $note_li; + $html = $(note.html); + gl.utils.localTimeAgo($('.js-timeago', $html)); + $html.syntaxHighlight(); + $html.find('.js-task-list-container').taskList('enable'); + $note_li = $('.note-row-' + note.id); + return $note_li.replaceWith($html); + }; + + + /* + Called in response to clicking the edit note link + + Replaces the note text with the note edit form + Adds a data attribute to the form with the original content of the note for cancellations + */ + + Notes.prototype.showEditForm = function(e, scrollTo, myLastNote) { + var $noteText, done, form, note; + e.preventDefault(); + note = $(this).closest(".note"); + note.addClass("is-editting"); + form = note.find(".note-edit-form"); + form.addClass('current-note-edit-form'); + note.find(".js-note-attachment-delete").show(); + done = function($noteText) { + var noteTextVal; + noteTextVal = $noteText.val(); + form.find('form.edit-note').data('original-note', noteTextVal); + return $noteText.val('').val(noteTextVal); + }; + new GLForm(form); + if ((scrollTo != null) && (myLastNote != null)) { + $('html, body').scrollTop($(document).height()); + return $('html, body').animate({ + scrollTop: myLastNote.offset().top - 150 + }, 500, function() { + var $noteText; + $noteText = form.find(".js-note-text"); + $noteText.focus(); + return done($noteText); + }); + } else { + $noteText = form.find('.js-note-text'); + $noteText.focus(); + return done($noteText); + } + }; + + + /* + Called in response to clicking the edit note link + + Hides edit form and restores the original note text to the editor textarea. + */ + + Notes.prototype.cancelEdit = function(e) { + var note; + e.preventDefault(); + note = $(e.target).closest('.note'); + return this.removeNoteEditForm(note); + }; + + Notes.prototype.removeNoteEditForm = function(note) { + var form; + form = note.find(".current-note-edit-form"); + note.removeClass("is-editting"); + form.removeClass("current-note-edit-form"); + return form.find(".js-note-text").val(form.find('form.edit-note').data('original-note')); + }; + + + /* + Called in response to deleting a note of any kind. + + Removes the actual note from view. + Removes the whole discussion if the last note is being removed. + */ + + Notes.prototype.removeNote = function(e) { + var noteId; + noteId = $(e.currentTarget).closest(".note").attr("id"); + $(".note[id='" + noteId + "']").each((function(_this) { + return function(i, el) { + var note, notes; + note = $(el); + notes = note.closest(".notes"); + if (notes.find(".note").length === 1) { + notes.closest(".timeline-entry").remove(); + notes.closest("tr").remove(); + } + return note.remove(); + }; + })(this)); + return this.updateNotesCount(-1); + }; + + + /* + Called in response to clicking the delete attachment link + + Removes the attachment wrapper view, including image tag if it exists + Resets the note editing form + */ + + Notes.prototype.removeAttachment = function() { + var note; + note = $(this).closest(".note"); + note.find(".note-attachment").remove(); + note.find(".note-body > .note-text").show(); + note.find(".note-header").show(); + return note.find(".current-note-edit-form").remove(); + }; + + + /* + Called when clicking on the "reply" button for a diff line. + + Shows the note form below the notes. + */ + + Notes.prototype.replyToDiscussionNote = function(e) { + var form, replyLink; + form = this.formClone.clone(); + replyLink = $(e.target).closest(".js-discussion-reply-button"); + replyLink.hide(); + replyLink.after(form); + return this.setupDiscussionNoteForm(replyLink, form); + }; + + + /* + Shows the diff or discussion form and does some setup on it. + + Sets some hidden fields in the form. + + Note: dataHolder must have the "discussionId", "lineCode", "noteableType" + and "noteableId" data attributes set. + */ + + Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) { + form.attr('id', "new-discussion-note-form-" + (dataHolder.data("discussionId"))); + form.attr("data-line-code", dataHolder.data("lineCode")); + form.find("#note_type").val(dataHolder.data("noteType")); + form.find("#line_type").val(dataHolder.data("lineType")); + form.find("#note_commit_id").val(dataHolder.data("commitId")); + form.find("#note_line_code").val(dataHolder.data("lineCode")); + form.find("#note_position").val(dataHolder.attr("data-position")); + form.find("#note_noteable_type").val(dataHolder.data("noteableType")); + form.find("#note_noteable_id").val(dataHolder.data("noteableId")); + form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text')); + this.setupNoteForm(form); + form.find(".js-note-text").focus(); + return form.removeClass('js-main-target-form').addClass("discussion-form js-discussion-note-form"); + }; + + + /* + Called when clicking on the "add a comment" button on the side of a diff line. + + Inserts a temporary row for the form below the line. + Sets up the form and shows it. + */ + + Notes.prototype.addDiffNote = function(e) { + var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, replyButton, row, rowCssToAdd, targetContent; + e.preventDefault(); + $link = $(e.currentTarget); + row = $link.closest("tr"); + nextRow = row.next(); + hasNotes = nextRow.is(".notes_holder"); + addForm = false; + targetContent = ".notes_content"; + rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"></td></tr>"; + if (this.isParallelView()) { + lineType = $link.data("lineType"); + targetContent += "." + lineType; + rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\"></td><td class=\"notes_content parallel old\"></td><td class=\"notes_line\"></td><td class=\"notes_content parallel new\"></td></tr>"; + } + if (hasNotes) { + notesContent = nextRow.find(targetContent); + if (notesContent.length) { + replyButton = notesContent.find(".js-discussion-reply-button:visible"); + if (replyButton.length) { + e.target = replyButton[0]; + $.proxy(this.replyToDiscussionNote, replyButton[0], e).call(); + } else { + noteForm = notesContent.find(".js-discussion-note-form"); + if (noteForm.length === 0) { + addForm = true; + } + } + } + } else { + row.after(rowCssToAdd); + addForm = true; + } + if (addForm) { + newForm = this.formClone.clone(); + newForm.appendTo(row.next().find(targetContent)); + return this.setupDiscussionNoteForm($link, newForm); + } + }; + + + /* + Called in response to "cancel" on a diff note form. + + Shows the reply button again. + Removes the form and if necessary it's temporary row. + */ + + Notes.prototype.removeDiscussionNoteForm = function(form) { + var glForm, row; + row = form.closest("tr"); + glForm = form.data('gl-form'); + glForm.destroy(); + form.find(".js-note-text").data("autosave").reset(); + form.prev(".js-discussion-reply-button").show(); + if (row.is(".js-temp-notes-holder")) { + return row.remove(); + } else { + return form.remove(); + } + }; + + Notes.prototype.cancelDiscussionForm = function(e) { + var form; + e.preventDefault(); + form = $(e.target).closest(".js-discussion-note-form"); + return this.removeDiscussionNoteForm(form); + }; + + + /* + Called after an attachment file has been selected. + + Updates the file name for the selected attachment. + */ + + Notes.prototype.updateFormAttachment = function() { + var filename, form; + form = $(this).closest("form"); + filename = $(this).val().replace(/^.*[\\\/]/, ""); + return form.find(".js-attachment-filename").text(filename); + }; + + + /* + Called when the tab visibility changes + */ + + Notes.prototype.visibilityChange = function() { + return this.refresh(); + }; + + Notes.prototype.updateCloseButton = function(e) { + var closebtn, form, textarea; + textarea = $(e.target); + form = textarea.parents('form'); + closebtn = form.find('.js-note-target-close'); + return closebtn.text(closebtn.data('original-text')); + }; + + Notes.prototype.updateTargetButtons = function(e) { + var closebtn, closetext, discardbtn, form, reopenbtn, reopentext, textarea; + textarea = $(e.target); + form = textarea.parents('form'); + reopenbtn = form.find('.js-note-target-reopen'); + closebtn = form.find('.js-note-target-close'); + discardbtn = form.find('.js-note-discard'); + if (textarea.val().trim().length > 0) { + reopentext = reopenbtn.data('alternative-text'); + closetext = closebtn.data('alternative-text'); + if (reopenbtn.text() !== reopentext) { + reopenbtn.text(reopentext); + } + if (closebtn.text() !== closetext) { + closebtn.text(closetext); + } + if (reopenbtn.is(':not(.btn-comment-and-reopen)')) { + reopenbtn.addClass('btn-comment-and-reopen'); + } + if (closebtn.is(':not(.btn-comment-and-close)')) { + closebtn.addClass('btn-comment-and-close'); + } + if (discardbtn.is(':hidden')) { + return discardbtn.show(); + } + } else { + reopentext = reopenbtn.data('original-text'); + closetext = closebtn.data('original-text'); + if (reopenbtn.text() !== reopentext) { + reopenbtn.text(reopentext); + } + if (closebtn.text() !== closetext) { + closebtn.text(closetext); + } + if (reopenbtn.is('.btn-comment-and-reopen')) { + reopenbtn.removeClass('btn-comment-and-reopen'); + } + if (closebtn.is('.btn-comment-and-close')) { + closebtn.removeClass('btn-comment-and-close'); + } + if (discardbtn.is(':visible')) { + return discardbtn.hide(); + } + } + }; + + Notes.prototype.initTaskList = function() { + this.enableTaskList(); + return $(document).on('tasklist:changed', '.note .js-task-list-container', this.updateTaskList); + }; + + Notes.prototype.enableTaskList = function() { + return $('.note .js-task-list-container').taskList('enable'); + }; + + Notes.prototype.updateTaskList = function() { + return $('form', this).submit(); + }; + + Notes.prototype.updateNotesCount = function(updateCount) { + return this.notesCountBadge.text(parseInt(this.notesCountBadge.text()) + updateCount); + }; + + return Notes; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee deleted file mode 100644 index 0ea54faae1a..00000000000 --- a/app/assets/javascripts/notes.js.coffee +++ /dev/null @@ -1,694 +0,0 @@ -#= require autosave -#= require autosize -#= require dropzone -#= require dropzone_input -#= require gfm_auto_complete -#= require jquery.atwho -#= require task_list - -class @Notes - @interval: null - - constructor: (notes_url, note_ids, last_fetched_at, view) -> - @notes_url = notes_url - @note_ids = note_ids - @last_fetched_at = last_fetched_at - @view = view - @noteable_url = document.URL - @notesCountBadge ||= $(".issuable-details").find(".notes-tab .badge") - @basePollingInterval = 15000 - @maxPollingSteps = 4 - - @cleanBinding() - @addBinding() - @setPollingInterval() - @setupMainTargetNoteForm() - @initTaskList() - - addBinding: -> - # add note to UI after creation - $(document).on "ajax:success", ".js-main-target-form", @addNote - $(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote - - # catch note ajax errors - $(document).on "ajax:error", ".js-main-target-form", @addNoteError - - # change note in UI after update - $(document).on "ajax:success", "form.edit-note", @updateNote - - # Edit note link - $(document).on "click", ".js-note-edit", @showEditForm - $(document).on "click", ".note-edit-cancel", @cancelEdit - - # Reopen and close actions for Issue/MR combined with note form submit - $(document).on "click", ".js-comment-button", @updateCloseButton - $(document).on "keyup input", ".js-note-text", @updateTargetButtons - - # remove a note (in general) - $(document).on "click", ".js-note-delete", @removeNote - - # delete note attachment - $(document).on "click", ".js-note-attachment-delete", @removeAttachment - - # reset main target form after submit - $(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton - $(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm - - # reset main target form when clicking discard - $(document).on "click", ".js-note-discard", @resetMainTargetForm - - # update the file name when an attachment is selected - $(document).on "change", ".js-note-attachment-input", @updateFormAttachment - - # reply to diff/discussion notes - $(document).on "click", ".js-discussion-reply-button", @replyToDiscussionNote - - # add diff note - $(document).on "click", ".js-add-diff-note-button", @addDiffNote - - # hide diff note form - $(document).on "click", ".js-close-discussion-note-form", @cancelDiscussionForm - - # fetch notes when tab becomes visible - $(document).on "visibilitychange", @visibilityChange - - # when issue status changes, we need to refresh data - $(document).on "issuable:change", @refresh - - # when a key is clicked on the notes - $(document).on "keydown", ".js-note-text", @keydownNoteText - - cleanBinding: -> - $(document).off "ajax:success", ".js-main-target-form" - $(document).off "ajax:success", ".js-discussion-note-form" - $(document).off "ajax:success", "form.edit-note" - $(document).off "click", ".js-note-edit" - $(document).off "click", ".note-edit-cancel" - $(document).off "click", ".js-note-delete" - $(document).off "click", ".js-note-attachment-delete" - $(document).off "ajax:complete", ".js-main-target-form" - $(document).off "ajax:success", ".js-main-target-form" - $(document).off "click", ".js-discussion-reply-button" - $(document).off "click", ".js-add-diff-note-button" - $(document).off "visibilitychange" - $(document).off "keyup", ".js-note-text" - $(document).off "click", ".js-note-target-reopen" - $(document).off "click", ".js-note-target-close" - $(document).off "click", ".js-note-discard" - $(document).off "keydown", ".js-note-text" - - $('.note .js-task-list-container').taskList('disable') - $(document).off 'tasklist:changed', '.note .js-task-list-container' - - keydownNoteText: (e) => - return if isMetaKey e - - $textarea = $(e.target) - - # Edit previous note when UP arrow is hit - switch e.which - when 38 - return unless $textarea.val() is '' - - myLastNote = $("li.note[data-author-id='#{gon.current_user_id}'][data-editable]:last") - if myLastNote.length - myLastNoteEditBtn = myLastNote.find('.js-note-edit') - myLastNoteEditBtn.trigger('click', [true, myLastNote]) - - # Cancel creating diff note or editing any note when ESCAPE is hit - when 27 - discussionNoteForm = $textarea.closest('.js-discussion-note-form') - if discussionNoteForm.length - if $textarea.val() isnt '' - return unless confirm('Are you sure you want to cancel creating this comment?') - - @removeDiscussionNoteForm(discussionNoteForm) - return - - editNote = $textarea.closest('.note') - if editNote.length - originalText = $textarea.closest('form').data('original-note') - newText = $textarea.val() - if originalText isnt newText - return unless confirm('Are you sure you want to cancel editing this comment?') - - @removeNoteEditForm(editNote) - - - isMetaKey = (e) -> - (e.metaKey or e.ctrlKey or e.altKey or e.shiftKey) - - initRefresh: -> - clearInterval(Notes.interval) - Notes.interval = setInterval => - @refresh() - , @pollingInterval - - refresh: => - if not document.hidden and document.URL.indexOf(@noteable_url) is 0 - @getContent() - - getContent: -> - return if @refreshing - - @refreshing = true - - $.ajax - url: @notes_url - data: "last_fetched_at=" + @last_fetched_at - dataType: "json" - success: (data) => - notes = data.notes - @last_fetched_at = data.last_fetched_at - @setPollingInterval(data.notes.length) - $.each notes, (i, note) => - if note.discussion_with_diff_html? - @renderDiscussionNote(note) - else - @renderNote(note) - .always () => - @refreshing = false - - ### - Increase @pollingInterval up to 120 seconds on every function call, - if `shouldReset` has a truthy value, 'null' or 'undefined' the variable - will reset to @basePollingInterval. - - Note: this function is used to gradually increase the polling interval - if there aren't new notes coming from the server - ### - setPollingInterval: (shouldReset = true) -> - nthInterval = @basePollingInterval * Math.pow(2, @maxPollingSteps - 1) - if shouldReset - @pollingInterval = @basePollingInterval - else if @pollingInterval < nthInterval - @pollingInterval *= 2 - - @initRefresh() - - ### - Render note in main comments area. - - Note: for rendering inline notes use renderDiscussionNote - ### - renderNote: (note) -> - unless note.valid - if note.award - new Flash('You have already awarded this emoji!', 'alert') - return - - if note.award - votesBlock = $('.js-awards-block').eq 0 - gl.awardsHandler.addAwardToEmojiBar votesBlock, note.name - gl.awardsHandler.scrollToAwards() - - # render note if it not present in loaded list - # or skip if rendered - else if @isNewNote(note) - @note_ids.push(note.id) - - $notesList = $('ul.main-notes-list') - - $notesList - .append(note.html) - .syntaxHighlight() - - # Update datetime format on the recent note - gl.utils.localTimeAgo($notesList.find("#note_#{note.id} .js-timeago"), false) - - @initTaskList() - @updateNotesCount(1) - - - ### - Check if note does not exists on page - ### - isNewNote: (note) -> - $.inArray(note.id, @note_ids) == -1 - - isParallelView: -> - @view == 'parallel' - - ### - Render note in discussion area. - - Note: for rendering inline notes use renderDiscussionNote - ### - renderDiscussionNote: (note) -> - return unless @isNewNote(note) - - @note_ids.push(note.id) - form = $("#new-discussion-note-form-#{note.discussion_id}") - if note.original_discussion_id? and form.length is 0 - form = $("#new-discussion-note-form-#{note.original_discussion_id}") - row = form.closest("tr") - note_html = $(note.html) - note_html.syntaxHighlight() - - # is this the first note of discussion? - discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']") - if note.original_discussion_id? and discussionContainer.length is 0 - discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']") - if discussionContainer.length is 0 - # insert the note and the reply button after the temp row - row.after note.discussion_html - - # remove the note (will be added again below) - row.next().find(".note").remove() - - # Before that, the container didn't exist - discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']") - - # Add note to 'Changes' page discussions - discussionContainer.append note_html - - # Init discussion on 'Discussion' page if it is merge request page - if $('body').attr('data-page').indexOf('projects:merge_request') is 0 - $('ul.main-notes-list') - .append(note.discussion_with_diff_html) - .syntaxHighlight() - else - # append new note to all matching discussions - discussionContainer.append note_html - - gl.utils.localTimeAgo($('.js-timeago', note_html), false) - - @updateNotesCount(1) - - ### - Called in response the main target form has been successfully submitted. - - Removes any errors. - Resets text and preview. - Resets buttons. - ### - resetMainTargetForm: (e) => - form = $(".js-main-target-form") - - # remove validation errors - form.find(".js-errors").remove() - - # reset text and preview - form.find(".js-md-write-button").click() - form.find(".js-note-text").val("").trigger "input" - - form.find(".js-note-text").data("autosave").reset() - - @updateTargetButtons(e) - - reenableTargetFormSubmitButton: -> - form = $(".js-main-target-form") - - form.find(".js-note-text").trigger "input" - - ### - Shows the main form and does some setup on it. - - Sets some hidden fields in the form. - ### - setupMainTargetNoteForm: -> - # find the form - form = $(".js-new-note-form") - - # Set a global clone of the form for later cloning - @formClone = form.clone() - - # show the form - @setupNoteForm(form) - - # fix classes - form.removeClass "js-new-note-form" - form.addClass "js-main-target-form" - - form.find("#note_line_code").remove() - form.find("#note_position").remove() - form.find("#note_type").remove() - - @parentTimeline = form.parents('.timeline') - - ### - General note form setup. - - deactivates the submit button when text is empty - hides the preview button when text is empty - setup GFM auto complete - show the form - ### - setupNoteForm: (form) -> - new GLForm form - - textarea = form.find(".js-note-text") - - new Autosave textarea, [ - "Note" - form.find("#note_noteable_type").val() - form.find("#note_noteable_id").val() - form.find("#note_commit_id").val() - form.find("#note_type").val() - form.find("#note_line_code").val() - form.find("#note_position").val() - ] - - ### - Called in response to the new note form being submitted - - Adds new note to list. - ### - addNote: (xhr, note, status) => - @renderNote(note) - - addNoteError: (xhr, note, status) => - new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', @parentTimeline) - - ### - Called in response to the new note form being submitted - - Adds new note to list. - ### - addDiscussionNote: (xhr, note, status) => - @renderDiscussionNote(note) - - # cleanup after successfully creating a diff/discussion note - @removeDiscussionNoteForm($(xhr.target)) - - ### - Called in response to the edit note form being submitted - - Updates the current note field. - ### - updateNote: (_xhr, note, _status) => - # Convert returned HTML to a jQuery object so we can modify it further - $html = $(note.html) - - gl.utils.localTimeAgo($('.js-timeago', $html)) - - $html.syntaxHighlight() - $html.find('.js-task-list-container').taskList('enable') - - # Find the note's `li` element by ID and replace it with the updated HTML - $note_li = $('.note-row-' + note.id) - $note_li.replaceWith($html) - - ### - Called in response to clicking the edit note link - - Replaces the note text with the note edit form - Adds a data attribute to the form with the original content of the note for cancellations - ### - showEditForm: (e, scrollTo, myLastNote) -> - e.preventDefault() - note = $(this).closest(".note") - note.addClass "is-editting" - form = note.find(".note-edit-form") - - form.addClass('current-note-edit-form') - - # Show the attachment delete link - note.find(".js-note-attachment-delete").show() - - done = ($noteText) -> - # Neat little trick to put the cursor at the end - noteTextVal = $noteText.val() - # Store the original note text in a data attribute to retrieve if a user cancels edit. - form.find('form.edit-note').data 'original-note', noteTextVal - $noteText.val('').val(noteTextVal); - - new GLForm form - if scrollTo? and myLastNote? - # scroll to the bottom - # so the open of the last element doesn't make a jump - $('html, body').scrollTop($(document).height()); - $('html, body').animate({ - scrollTop: myLastNote.offset().top - 150 - }, 500, -> - $noteText = form.find(".js-note-text") - $noteText.focus() - done($noteText) - ); - else - $noteText = form.find('.js-note-text') - $noteText.focus() - done($noteText) - - ### - Called in response to clicking the edit note link - - Hides edit form and restores the original note text to the editor textarea. - ### - cancelEdit: (e) => - e.preventDefault() - note = $(e.target).closest('.note') - @removeNoteEditForm(note) - - removeNoteEditForm: (note) -> - form = note.find(".current-note-edit-form") - note.removeClass "is-editting" - form.removeClass("current-note-edit-form") - # Replace markdown textarea text with original note text. - form.find(".js-note-text").val(form.find('form.edit-note').data('original-note')) - - ### - Called in response to deleting a note of any kind. - - Removes the actual note from view. - Removes the whole discussion if the last note is being removed. - ### - removeNote: (e) => - noteId = $(e.currentTarget) - .closest(".note") - .attr("id") - - # A same note appears in the "Discussion" and in the "Changes" tab, we have - # to remove all. Using $(".note[id='noteId']") ensure we get all the notes, - # where $("#noteId") would return only one. - $(".note[id='#{noteId}']").each (i, el) => - note = $(el) - notes = note.closest(".notes") - - # check if this is the last note for this line - if notes.find(".note").length is 1 - - # "Discussions" tab - notes.closest(".timeline-entry").remove() - - # "Changes" tab / commit view - notes.closest("tr").remove() - - note.remove() - - # Decrement the "Discussions" counter only once - @updateNotesCount(-1) - - ### - Called in response to clicking the delete attachment link - - Removes the attachment wrapper view, including image tag if it exists - Resets the note editing form - ### - removeAttachment: -> - note = $(this).closest(".note") - note.find(".note-attachment").remove() - note.find(".note-body > .note-text").show() - note.find(".note-header").show() - note.find(".current-note-edit-form").remove() - - ### - Called when clicking on the "reply" button for a diff line. - - Shows the note form below the notes. - ### - replyToDiscussionNote: (e) => - form = @formClone.clone() - replyLink = $(e.target).closest(".js-discussion-reply-button") - replyLink.hide() - - # insert the form after the button - replyLink.after form - - # show the form - @setupDiscussionNoteForm(replyLink, form) - - ### - Shows the diff or discussion form and does some setup on it. - - Sets some hidden fields in the form. - - Note: dataHolder must have the "discussionId", "lineCode", "noteableType" - and "noteableId" data attributes set. - ### - setupDiscussionNoteForm: (dataHolder, form) => - # setup note target - form.attr 'id', "new-discussion-note-form-#{dataHolder.data("discussionId")}" - form.attr "data-line-code", dataHolder.data("lineCode") - form.find("#note_type").val dataHolder.data("noteType") - form.find("#line_type").val dataHolder.data("lineType") - form.find("#note_commit_id").val dataHolder.data("commitId") - form.find("#note_line_code").val dataHolder.data("lineCode") - form.find("#note_position").val dataHolder.attr("data-position") - form.find("#note_noteable_type").val dataHolder.data("noteableType") - form.find("#note_noteable_id").val dataHolder.data("noteableId") - form.find('.js-note-discard') - .show() - .removeClass('js-note-discard') - .addClass('js-close-discussion-note-form') - .text(form.find('.js-close-discussion-note-form').data('cancel-text')) - @setupNoteForm form - form.find(".js-note-text").focus() - form - .removeClass('js-main-target-form') - .addClass("discussion-form js-discussion-note-form") - - ### - Called when clicking on the "add a comment" button on the side of a diff line. - - Inserts a temporary row for the form below the line. - Sets up the form and shows it. - ### - addDiffNote: (e) => - e.preventDefault() - $link = $(e.currentTarget) - row = $link.closest("tr") - nextRow = row.next() - hasNotes = nextRow.is(".notes_holder") - addForm = false - targetContent = ".notes_content" - rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"></td></tr>" - - # In parallel view, look inside the correct left/right pane - if @isParallelView() - lineType = $link.data("lineType") - targetContent += "." + lineType - rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\"></td><td class=\"notes_content parallel old\"></td><td class=\"notes_line\"></td><td class=\"notes_content parallel new\"></td></tr>" - - if hasNotes - notesContent = nextRow.find(targetContent) - if notesContent.length - replyButton = notesContent.find(".js-discussion-reply-button:visible") - if replyButton.length - e.target = replyButton[0] - $.proxy(@replyToDiscussionNote, replyButton[0], e).call() - else - # In parallel view, the form may not be present in one of the panes - noteForm = notesContent.find(".js-discussion-note-form") - if noteForm.length == 0 - addForm = true - else - # add a notes row and insert the form - row.after rowCssToAdd - addForm = true - - if addForm - newForm = @formClone.clone() - newForm.appendTo row.next().find(targetContent) - - # show the form - @setupDiscussionNoteForm $link, newForm - - ### - Called in response to "cancel" on a diff note form. - - Shows the reply button again. - Removes the form and if necessary it's temporary row. - ### - removeDiscussionNoteForm: (form)-> - row = form.closest("tr") - - glForm = form.data 'gl-form' - glForm.destroy() - - form.find(".js-note-text").data("autosave").reset() - - # show the reply button (will only work for replies) - form.prev(".js-discussion-reply-button").show() - if row.is(".js-temp-notes-holder") - # remove temporary row for diff lines - row.remove() - else - # only remove the form - form.remove() - - cancelDiscussionForm: (e) => - e.preventDefault() - form = $(e.target).closest(".js-discussion-note-form") - @removeDiscussionNoteForm(form) - - ### - Called after an attachment file has been selected. - - Updates the file name for the selected attachment. - ### - updateFormAttachment: -> - form = $(this).closest("form") - - # get only the basename - filename = $(this).val().replace(/^.*[\\\/]/, "") - form.find(".js-attachment-filename").text filename - - ### - Called when the tab visibility changes - ### - visibilityChange: => - @refresh() - - updateCloseButton: (e) => - textarea = $(e.target) - form = textarea.parents('form') - closebtn = form.find('.js-note-target-close') - closebtn.text(closebtn.data('original-text')) - - updateTargetButtons: (e) => - textarea = $(e.target) - form = textarea.parents('form') - reopenbtn = form.find('.js-note-target-reopen') - closebtn = form.find('.js-note-target-close') - discardbtn = form.find('.js-note-discard') - - if textarea.val().trim().length > 0 - reopentext = reopenbtn.data('alternative-text') - closetext = closebtn.data('alternative-text') - - if reopenbtn.text() isnt reopentext - reopenbtn.text(reopentext) - - if closebtn.text() isnt closetext - closebtn.text(closetext) - - if reopenbtn.is(':not(.btn-comment-and-reopen)') - reopenbtn.addClass('btn-comment-and-reopen') - - if closebtn.is(':not(.btn-comment-and-close)') - closebtn.addClass('btn-comment-and-close') - - if discardbtn.is(':hidden') - discardbtn.show() - else - reopentext = reopenbtn.data('original-text') - closetext = closebtn.data('original-text') - - if reopenbtn.text() isnt reopentext - reopenbtn.text(reopentext) - - if closebtn.text() isnt closetext - closebtn.text(closetext) - - if reopenbtn.is('.btn-comment-and-reopen') - reopenbtn.removeClass('btn-comment-and-reopen') - - if closebtn.is('.btn-comment-and-close') - closebtn.removeClass('btn-comment-and-close') - - if discardbtn.is(':visible') - discardbtn.hide() - - initTaskList: -> - @enableTaskList() - $(document).on 'tasklist:changed', '.note .js-task-list-container', @updateTaskList - - enableTaskList: -> - $('.note .js-task-list-container').taskList('enable') - - updateTaskList: -> - $('form', this).submit() - - updateNotesCount: (updateCount) -> - @notesCountBadge.text(parseInt(@notesCountBadge.text()) + updateCount) diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js new file mode 100644 index 00000000000..a41e9d3fabe --- /dev/null +++ b/app/assets/javascripts/notifications_dropdown.js @@ -0,0 +1,30 @@ +(function() { + this.NotificationsDropdown = (function() { + function NotificationsDropdown() { + $(document).off('click', '.update-notification').on('click', '.update-notification', function(e) { + var form, label, notificationLevel; + e.preventDefault(); + if ($(this).is('.is-active') && $(this).data('notification-level') === 'custom') { + return; + } + notificationLevel = $(this).data('notification-level'); + label = $(this).data('notification-title'); + form = $(this).parents('.notification-form:first'); + form.find('.js-notification-loading').toggleClass('fa-bell fa-spin fa-spinner'); + form.find('#notification_setting_level').val(notificationLevel); + return form.submit(); + }); + $(document).off('ajax:success', '.notification-form').on('ajax:success', '.notification-form', function(e, data) { + if (data.saved) { + return $(e.currentTarget).closest('.notification-dropdown').replaceWith(data.html); + } else { + return new Flash('Failed to save new settings', 'alert'); + } + }); + } + + return NotificationsDropdown; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/notifications_dropdown.js.coffee b/app/assets/javascripts/notifications_dropdown.js.coffee deleted file mode 100644 index 0bbd082c156..00000000000 --- a/app/assets/javascripts/notifications_dropdown.js.coffee +++ /dev/null @@ -1,25 +0,0 @@ -class @NotificationsDropdown - constructor: -> - $(document) - .off 'click', '.update-notification' - .on 'click', '.update-notification', (e) -> - e.preventDefault() - - return if $(this).is('.is-active') and $(this).data('notification-level') is 'custom' - - notificationLevel = $(@).data 'notification-level' - label = $(@).data 'notification-title' - form = $(this).parents('.notification-form:first') - form.find('.js-notification-loading').toggleClass 'fa-bell fa-spin fa-spinner' - form.find('#notification_setting_level').val(notificationLevel) - form.submit() - - $(document) - .off 'ajax:success', '.notification-form' - .on 'ajax:success', '.notification-form', (e, data) -> - if data.saved - $(e.currentTarget) - .closest('.notification-dropdown') - .replaceWith(data.html) - else - new Flash('Failed to save new settings', 'alert') diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js new file mode 100644 index 00000000000..6b2ef17ef6b --- /dev/null +++ b/app/assets/javascripts/notifications_form.js @@ -0,0 +1,58 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.NotificationsForm = (function() { + function NotificationsForm() { + this.toggleCheckbox = bind(this.toggleCheckbox, this); + this.removeEventListeners(); + this.initEventListeners(); + } + + NotificationsForm.prototype.removeEventListeners = function() { + return $(document).off('change', '.js-custom-notification-event'); + }; + + NotificationsForm.prototype.initEventListeners = function() { + return $(document).on('change', '.js-custom-notification-event', this.toggleCheckbox); + }; + + NotificationsForm.prototype.toggleCheckbox = function(e) { + var $checkbox, $parent; + $checkbox = $(e.currentTarget); + $parent = $checkbox.closest('.checkbox'); + return this.saveEvent($checkbox, $parent); + }; + + NotificationsForm.prototype.showCheckboxLoadingSpinner = function($parent) { + return $parent.addClass('is-loading').find('.custom-notification-event-loading').removeClass('fa-check').addClass('fa-spin fa-spinner').removeClass('is-done'); + }; + + NotificationsForm.prototype.saveEvent = function($checkbox, $parent) { + var form; + form = $parent.parents('form:first'); + return $.ajax({ + url: form.attr('action'), + method: form.attr('method'), + dataType: 'json', + data: form.serialize(), + beforeSend: (function(_this) { + return function() { + return _this.showCheckboxLoadingSpinner($parent); + }; + })(this) + }).done(function(data) { + $checkbox.enable(); + if (data.saved) { + $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done'); + return setTimeout(function() { + return $parent.removeClass('is-loading').find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done'); + }, 2000); + } + }); + }; + + return NotificationsForm; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/notifications_form.js.coffee b/app/assets/javascripts/notifications_form.js.coffee deleted file mode 100644 index 3432428702a..00000000000 --- a/app/assets/javascripts/notifications_form.js.coffee +++ /dev/null @@ -1,49 +0,0 @@ -class @NotificationsForm - constructor: -> - @removeEventListeners() - @initEventListeners() - - removeEventListeners: -> - $(document).off 'change', '.js-custom-notification-event' - - initEventListeners: -> - $(document).on 'change', '.js-custom-notification-event', @toggleCheckbox - - toggleCheckbox: (e) => - $checkbox = $(e.currentTarget) - $parent = $checkbox.closest('.checkbox') - @saveEvent($checkbox, $parent) - - showCheckboxLoadingSpinner: ($parent) -> - $parent - .addClass 'is-loading' - .find '.custom-notification-event-loading' - .removeClass 'fa-check' - .addClass 'fa-spin fa-spinner' - .removeClass 'is-done' - - saveEvent: ($checkbox, $parent) -> - form = $parent.parents('form:first') - - $.ajax( - url: form.attr('action') - method: form.attr('method') - dataType: 'json' - data: form.serialize() - - beforeSend: => - @showCheckboxLoadingSpinner($parent) - ).done (data) -> - $checkbox.enable() - - if data.saved - $parent - .find '.custom-notification-event-loading' - .toggleClass 'fa-spin fa-spinner fa-check is-done' - - setTimeout(-> - $parent - .removeClass 'is-loading' - .find '.custom-notification-event-loading' - .toggleClass 'fa-spin fa-spinner fa-check is-done' - , 2000) diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js new file mode 100644 index 00000000000..b81ed50cb48 --- /dev/null +++ b/app/assets/javascripts/pager.js @@ -0,0 +1,63 @@ +(function() { + this.Pager = { + init: function(limit, preload, disable, callback) { + this.limit = limit != null ? limit : 0; + this.disable = disable != null ? disable : false; + this.callback = callback != null ? callback : $.noop; + this.loading = $('.loading').first(); + if (preload) { + this.offset = 0; + this.getOld(); + } else { + this.offset = this.limit; + } + return this.initLoadMore(); + }, + getOld: function() { + this.loading.show(); + return $.ajax({ + type: "GET", + url: $(".content_list").data('href') || location.href, + data: "limit=" + this.limit + "&offset=" + this.offset, + complete: (function(_this) { + return function() { + return _this.loading.hide(); + }; + })(this), + success: function(data) { + Pager.append(data.count, data.html); + return Pager.callback(); + }, + dataType: "json" + }); + }, + append: function(count, html) { + $(".content_list").append(html); + if (count > 0) { + return this.offset += count; + } else { + return this.disable = true; + } + }, + initLoadMore: function() { + $(document).unbind('scroll'); + return $(document).endlessScroll({ + bottomPixels: 400, + fireDelay: 1000, + fireOnce: true, + ceaseFire: function() { + return Pager.disable; + }, + callback: (function(_this) { + return function(i) { + if (!_this.loading.is(':visible')) { + _this.loading.show(); + return Pager.getOld(); + } + }; + })(this) + }); + } + }; + +}).call(this); diff --git a/app/assets/javascripts/pager.js.coffee b/app/assets/javascripts/pager.js.coffee deleted file mode 100644 index 8049c5c30e2..00000000000 --- a/app/assets/javascripts/pager.js.coffee +++ /dev/null @@ -1,44 +0,0 @@ -@Pager = - init: (@limit = 0, preload, @disable = false, @callback = $.noop) -> - @loading = $('.loading').first() - - if preload - @offset = 0 - @getOld() - else - @offset = @limit - @initLoadMore() - - getOld: -> - @loading.show() - $.ajax - type: "GET" - url: $(".content_list").data('href') || location.href - data: "limit=" + @limit + "&offset=" + @offset - complete: => - @loading.hide() - success: (data) -> - Pager.append(data.count, data.html) - Pager.callback() - dataType: "json" - - append: (count, html) -> - $(".content_list").append html - if count > 0 - @offset += count - else - @disable = true - - initLoadMore: -> - $(document).unbind('scroll') - $(document).endlessScroll - bottomPixels: 400 - fireDelay: 1000 - fireOnce: true - ceaseFire: -> - Pager.disable - - callback: (i) => - unless @loading.is(':visible') - @loading.show() - Pager.getOld() diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js new file mode 100644 index 00000000000..a3eea316f67 --- /dev/null +++ b/app/assets/javascripts/profile/gl_crop.js @@ -0,0 +1,169 @@ +(function() { + var GitLabCrop, + bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + GitLabCrop = (function() { + var FILENAMEREGEX; + + FILENAMEREGEX = /^.*[\\\/]/; + + function GitLabCrop(input, opts) { + var ref, ref1, ref2, ref3, ref4; + if (opts == null) { + opts = {}; + } + this.onUploadImageBtnClick = bind(this.onUploadImageBtnClick, this); + this.onModalHide = bind(this.onModalHide, this); + this.onModalShow = bind(this.onModalShow, this); + this.onPickImageClick = bind(this.onPickImageClick, this); + this.fileInput = $(input); + this.fileInput.attr('name', (this.fileInput.attr('name')) + "-trigger").attr('id', (this.fileInput.attr('id')) + "-trigger"); + this.exportWidth = (ref = opts.exportWidth) != null ? ref : 200, this.exportHeight = (ref1 = opts.exportHeight) != null ? ref1 : 200, this.cropBoxWidth = (ref2 = opts.cropBoxWidth) != null ? ref2 : 200, this.cropBoxHeight = (ref3 = opts.cropBoxHeight) != null ? ref3 : 200, this.form = (ref4 = opts.form) != null ? ref4 : this.fileInput.parents('form'), this.filename = opts.filename, this.previewImage = opts.previewImage, this.modalCrop = opts.modalCrop, this.pickImageEl = opts.pickImageEl, this.uploadImageBtn = opts.uploadImageBtn, this.modalCropImg = opts.modalCropImg; + this.filename = this.getElement(this.filename); + this.previewImage = this.getElement(this.previewImage); + this.pickImageEl = this.getElement(this.pickImageEl); + this.modalCrop = _.isString(this.modalCrop) ? $(this.modalCrop) : this.modalCrop; + this.uploadImageBtn = _.isString(this.uploadImageBtn) ? $(this.uploadImageBtn) : this.uploadImageBtn; + this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg; + this.cropActionsBtn = this.modalCrop.find('[data-method]'); + this.bindEvents(); + } + + GitLabCrop.prototype.getElement = function(selector) { + return $(selector, this.form); + }; + + GitLabCrop.prototype.bindEvents = function() { + var _this; + _this = this; + this.fileInput.on('change', function(e) { + return _this.onFileInputChange(e, this); + }); + this.pickImageEl.on('click', this.onPickImageClick); + this.modalCrop.on('shown.bs.modal', this.onModalShow); + this.modalCrop.on('hidden.bs.modal', this.onModalHide); + this.uploadImageBtn.on('click', this.onUploadImageBtnClick); + this.cropActionsBtn.on('click', function(e) { + var btn; + btn = this; + return _this.onActionBtnClick(btn); + }); + return this.croppedImageBlob = null; + }; + + GitLabCrop.prototype.onPickImageClick = function() { + return this.fileInput.trigger('click'); + }; + + GitLabCrop.prototype.onModalShow = function() { + var _this; + _this = this; + return this.modalCropImg.cropper({ + viewMode: 1, + center: false, + aspectRatio: 1, + modal: true, + scalable: false, + rotatable: false, + zoomable: true, + dragMode: 'move', + guides: false, + zoomOnTouch: false, + zoomOnWheel: false, + cropBoxMovable: false, + cropBoxResizable: false, + toggleDragModeOnDblclick: false, + built: function() { + var $image, container, cropBoxHeight, cropBoxWidth; + $image = $(this); + container = $image.cropper('getContainerData'); + cropBoxWidth = _this.cropBoxWidth; + cropBoxHeight = _this.cropBoxHeight; + return $image.cropper('setCropBoxData', { + width: cropBoxWidth, + height: cropBoxHeight, + left: (container.width - cropBoxWidth) / 2, + top: (container.height - cropBoxHeight) / 2 + }); + } + }); + }; + + GitLabCrop.prototype.onModalHide = function() { + return this.modalCropImg.attr('src', '').cropper('destroy'); + }; + + GitLabCrop.prototype.onUploadImageBtnClick = function(e) { + e.preventDefault(); + this.setBlob(); + this.setPreview(); + this.modalCrop.modal('hide'); + return this.fileInput.val(''); + }; + + GitLabCrop.prototype.onActionBtnClick = function(btn) { + var data, result; + data = $(btn).data(); + if (this.modalCropImg.data('cropper') && data.method) { + return result = this.modalCropImg.cropper(data.method, data.option); + } + }; + + GitLabCrop.prototype.onFileInputChange = function(e, input) { + return this.readFile(input); + }; + + GitLabCrop.prototype.readFile = function(input) { + var _this, reader; + _this = this; + reader = new FileReader; + reader.onload = function() { + _this.modalCropImg.attr('src', reader.result); + return _this.modalCrop.modal('show'); + }; + return reader.readAsDataURL(input.files[0]); + }; + + GitLabCrop.prototype.dataURLtoBlob = function(dataURL) { + var array, binary, i, k, len, v; + binary = atob(dataURL.split(',')[1]); + array = []; + for (k = i = 0, len = binary.length; i < len; k = ++i) { + v = binary[k]; + array.push(binary.charCodeAt(k)); + } + return new Blob([new Uint8Array(array)], { + type: 'image/png' + }); + }; + + GitLabCrop.prototype.setPreview = function() { + var filename; + this.previewImage.attr('src', this.dataURL); + filename = this.fileInput.val().replace(FILENAMEREGEX, ''); + return this.filename.text(filename); + }; + + GitLabCrop.prototype.setBlob = function() { + this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', { + width: 200, + height: 200 + }).toDataURL('image/png'); + return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL); + }; + + GitLabCrop.prototype.getBlob = function() { + return this.croppedImageBlob; + }; + + return GitLabCrop; + + })(); + + $.fn.glCrop = function(opts) { + return this.each(function() { + return $(this).data('glcrop', new GitLabCrop(this, opts)); + }); + }; + +}).call(this); diff --git a/app/assets/javascripts/profile/gl_crop.js.coffee b/app/assets/javascripts/profile/gl_crop.js.coffee deleted file mode 100644 index df9bfdfa6cc..00000000000 --- a/app/assets/javascripts/profile/gl_crop.js.coffee +++ /dev/null @@ -1,152 +0,0 @@ -class GitLabCrop - # Matches everything but the file name - FILENAMEREGEX = /^.*[\\\/]/ - - constructor: (input, opts = {}) -> - @fileInput = $(input) - - # We should rename to avoid spec to fail - # Form will submit the proper input filed with a file using FormData - @fileInput - .attr('name', "#{@fileInput.attr('name')}-trigger") - .attr('id', "#{@fileInput.attr('id')}-trigger") - - # Set defaults - { - @exportWidth = 200 - @exportHeight = 200 - @cropBoxWidth = 200 - @cropBoxHeight = 200 - @form = @fileInput.parents('form') - - # Required params - @filename - @previewImage - @modalCrop - @pickImageEl - @uploadImageBtn - @modalCropImg - } = opts - - # Ensure needed elements are jquery objects - # If selector is provided we will convert them to a jQuery Object - @filename = @getElement(@filename) - @previewImage = @getElement(@previewImage) - @pickImageEl = @getElement(@pickImageEl) - - # Modal elements usually are outside the @form element - @modalCrop = if _.isString(@modalCrop) then $(@modalCrop) else @modalCrop - @uploadImageBtn = if _.isString(@uploadImageBtn) then $(@uploadImageBtn) else @uploadImageBtn - @modalCropImg = if _.isString(@modalCropImg) then $(@modalCropImg) else @modalCropImg - - @cropActionsBtn = @modalCrop.find('[data-method]') - - @bindEvents() - - getElement: (selector) -> - $(selector, @form) - - bindEvents: -> - _this = @ - @fileInput.on 'change', (e) -> - _this.onFileInputChange(e, @) - - @pickImageEl.on 'click', @onPickImageClick - @modalCrop.on 'shown.bs.modal', @onModalShow - @modalCrop.on 'hidden.bs.modal', @onModalHide - @uploadImageBtn.on 'click', @onUploadImageBtnClick - @cropActionsBtn.on 'click', (e) -> - btn = @ - _this.onActionBtnClick(btn) - @croppedImageBlob = null - - onPickImageClick: => - @fileInput.trigger('click') - - onModalShow: => - _this = @ - @modalCropImg.cropper( - viewMode: 1 - center: false - aspectRatio: 1 - modal: true - scalable: false - rotatable: false - zoomable: true - dragMode: 'move' - guides: false - zoomOnTouch: false - zoomOnWheel: false - cropBoxMovable: false - cropBoxResizable: false - toggleDragModeOnDblclick: false - built: -> - $image = $(@) - container = $image.cropper 'getContainerData' - cropBoxWidth = _this.cropBoxWidth; - cropBoxHeight = _this.cropBoxHeight; - - $image.cropper('setCropBoxData', - width: cropBoxWidth, - height: cropBoxHeight, - left: (container.width - cropBoxWidth) / 2, - top: (container.height - cropBoxHeight) / 2 - ) - ) - - - onModalHide: => - @modalCropImg - .attr('src', '') # Remove attached image - .cropper('destroy') # Destroy cropper instance - - onUploadImageBtnClick: (e) => - e.preventDefault() - @setBlob() - @setPreview() - @modalCrop.modal('hide') - @fileInput.val('') - - onActionBtnClick: (btn) -> - data = $(btn).data() - - if @modalCropImg.data('cropper') && data.method - result = @modalCropImg.cropper data.method, data.option - - onFileInputChange: (e, input) -> - @readFile(input) - - readFile: (input) -> - _this = @ - reader = new FileReader - reader.onload = -> - _this.modalCropImg.attr('src', reader.result) - _this.modalCrop.modal('show') - - reader.readAsDataURL(input.files[0]) - - dataURLtoBlob: (dataURL) -> - binary = atob(dataURL.split(',')[1]) - array = [] - for v, k in binary - array.push(binary.charCodeAt(k)) - new Blob([new Uint8Array(array)], type: 'image/png') - - setPreview: -> - @previewImage.attr('src', @dataURL) - filename = @fileInput.val().replace(FILENAMEREGEX, '') - @filename.text(filename) - - setBlob: -> - @dataURL = @modalCropImg.cropper('getCroppedCanvas', - width: 200 - height: 200 - ).toDataURL('image/png') - @croppedImageBlob = @dataURLtoBlob(@dataURL) - - getBlob: -> - @croppedImageBlob - -$.fn.glCrop = (opts) -> - return @.each -> - $(@).data('glcrop', new GitLabCrop(@, opts)) diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js new file mode 100644 index 00000000000..ed1d87abafe --- /dev/null +++ b/app/assets/javascripts/profile/profile.js @@ -0,0 +1,102 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Profile = (function() { + function Profile(opts) { + var cropOpts, ref; + if (opts == null) { + opts = {}; + } + this.onSubmitForm = bind(this.onSubmitForm, this); + this.form = (ref = opts.form) != null ? ref : $('.edit-user'); + $('.js-preferences-form').on('change.preference', 'input[type=radio]', function() { + return $(this).parents('form').submit(); + }); + $('#user_notification_email').on('change', function() { + return $(this).parents('form').submit(); + }); + $('.update-username').on('ajax:before', function() { + $('.loading-username').show(); + $(this).find('.update-success').hide(); + return $(this).find('.update-failed').hide(); + }); + $('.update-username').on('ajax:complete', function() { + $('.loading-username').hide(); + $(this).find('.btn-save').enable(); + return $(this).find('.loading-gif').hide(); + }); + $('.update-notifications').on('ajax:success', function(e, data) { + if (data.saved) { + return new Flash("Notification settings saved", "notice"); + } else { + return new Flash("Failed to save new settings", "alert"); + } + }); + this.bindEvents(); + cropOpts = { + filename: '.js-avatar-filename', + previewImage: '.avatar-image .avatar', + modalCrop: '.modal-profile-crop', + pickImageEl: '.js-choose-user-avatar-button', + uploadImageBtn: '.js-upload-user-avatar', + modalCropImg: '.modal-profile-crop-image' + }; + this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop'); + } + + Profile.prototype.bindEvents = function() { + return this.form.on('submit', this.onSubmitForm); + }; + + Profile.prototype.onSubmitForm = function(e) { + e.preventDefault(); + return this.saveForm(); + }; + + Profile.prototype.saveForm = function() { + var avatarBlob, formData, self; + self = this; + formData = new FormData(this.form[0]); + avatarBlob = this.avatarGlCrop.getBlob(); + if (avatarBlob != null) { + formData.append('user[avatar]', avatarBlob, 'avatar.png'); + } + return $.ajax({ + url: this.form.attr('action'), + type: this.form.attr('method'), + data: formData, + dataType: "json", + processData: false, + contentType: false, + success: function(response) { + return new Flash(response.message, 'notice'); + }, + error: function(jqXHR) { + return new Flash(jqXHR.responseJSON.message, 'alert'); + }, + complete: function() { + window.scrollTo(0, 0); + return self.form.find(':input[disabled]').enable(); + } + }); + }; + + return Profile; + + })(); + + $(function() { + $(document).on('focusout.ssh_key', '#key_key', function() { + var $title, comment; + $title = $('#key_title'); + comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/); + if (comment && comment.length > 1 && $title.val() === '') { + return $title.val(comment[1]).change(); + } + }); + if (gl.utils.getPagePath() === 'profiles') { + return new Profile(); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/profile/profile.js.coffee b/app/assets/javascripts/profile/profile.js.coffee deleted file mode 100644 index f3b05f2c646..00000000000 --- a/app/assets/javascripts/profile/profile.js.coffee +++ /dev/null @@ -1,83 +0,0 @@ -class @Profile - constructor: (opts = {}) -> - { - @form = $('.edit-user') - } = opts - - # Automatically submit the Preferences form when any of its radio buttons change - $('.js-preferences-form').on 'change.preference', 'input[type=radio]', -> - $(this).parents('form').submit() - - # Automatically submit email form when it changes - $('#user_notification_email').on 'change', -> - $(this).parents('form').submit() - - $('.update-username').on 'ajax:before', -> - $('.loading-username').show() - $(this).find('.update-success').hide() - $(this).find('.update-failed').hide() - - $('.update-username').on 'ajax:complete', -> - $('.loading-username').hide() - $(this).find('.btn-save').enable() - $(this).find('.loading-gif').hide() - - $('.update-notifications').on 'ajax:success', (e, data) -> - if data.saved - new Flash("Notification settings saved", "notice") - else - new Flash("Failed to save new settings", "alert") - - @bindEvents() - - cropOpts = - filename: '.js-avatar-filename' - previewImage: '.avatar-image .avatar' - modalCrop: '.modal-profile-crop' - pickImageEl: '.js-choose-user-avatar-button' - uploadImageBtn: '.js-upload-user-avatar' - modalCropImg: '.modal-profile-crop-image' - - @avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data 'glcrop' - - bindEvents: -> - @form.on 'submit', @onSubmitForm - - onSubmitForm: (e) => - e.preventDefault() - @saveForm() - - saveForm: -> - self = @ - formData = new FormData(@form[0]) - - avatarBlob = @avatarGlCrop.getBlob() - formData.append('user[avatar]', avatarBlob, 'avatar.png') if avatarBlob? - - $.ajax - url: @form.attr('action') - type: @form.attr('method') - data: formData - dataType: "json" - processData: false - contentType: false - success: (response) -> - new Flash(response.message, 'notice') - error: (jqXHR) -> - new Flash(jqXHR.responseJSON.message, 'alert') - complete: -> - window.scrollTo 0, 0 - # Enable submit button after requests ends - self.form.find(':input[disabled]').enable() - -$ -> - # Extract the SSH Key title from its comment - $(document).on 'focusout.ssh_key', '#key_key', -> - $title = $('#key_title') - comment = $(@).val().match(/^\S+ \S+ (.+)\n?$/) - - if comment && comment.length > 1 && $title.val() == '' - $title.val(comment[1]).change() - - if gl.utils.getPagePath() == 'profiles' - new Profile() diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js new file mode 100644 index 00000000000..b95faadc8e7 --- /dev/null +++ b/app/assets/javascripts/profile/profile_bundle.js @@ -0,0 +1,7 @@ + +/*= require_tree . */ + +(function() { + + +}).call(this); diff --git a/app/assets/javascripts/profile/profile_bundle.js.coffee b/app/assets/javascripts/profile/profile_bundle.js.coffee deleted file mode 100644 index 91cacfece46..00000000000 --- a/app/assets/javascripts/profile/profile_bundle.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -# -#= require_tree . diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js new file mode 100644 index 00000000000..e6663177161 --- /dev/null +++ b/app/assets/javascripts/project.js @@ -0,0 +1,103 @@ +(function() { + this.Project = (function() { + function Project() { + $('ul.clone-options-dropdown a').click(function() { + var url; + if ($(this).hasClass('active')) { + return; + } + $('.active').not($(this)).removeClass('active'); + $(this).toggleClass('active'); + url = $("#project_clone").val(); + $('#project_clone').val(url); + return $('.clone').text(url); + }); + this.initRefSwitcher(); + $('.project-refs-select').on('change', function() { + return $(this).parents('form').submit(); + }); + $('.hide-no-ssh-message').on('click', function(e) { + var path; + path = '/'; + $.cookie('hide_no_ssh_message', 'false', { + path: path + }); + $(this).parents('.no-ssh-key-message').remove(); + return e.preventDefault(); + }); + $('.hide-no-password-message').on('click', function(e) { + var path; + path = '/'; + $.cookie('hide_no_password_message', 'false', { + path: path + }); + $(this).parents('.no-password-message').remove(); + return e.preventDefault(); + }); + this.projectSelectDropdown(); + } + + Project.prototype.projectSelectDropdown = function() { + new ProjectSelect(); + $('.project-item-select').on('click', (function(_this) { + return function(e) { + return _this.changeProject($(e.currentTarget).val()); + }; + })(this)); + return $('.js-projects-dropdown-toggle').on('click', function(e) { + e.preventDefault(); + return $('.js-projects-dropdown').select2('open'); + }); + }; + + Project.prototype.changeProject = function(url) { + return window.location = url; + }; + + Project.prototype.initRefSwitcher = function() { + return $('.js-project-refs-dropdown').each(function() { + var $dropdown, selected; + $dropdown = $(this); + selected = $dropdown.data('selected'); + return $dropdown.glDropdown({ + data: function(term, callback) { + return $.ajax({ + url: $dropdown.data('refs-url'), + data: { + ref: $dropdown.data('ref') + } + }).done(function(refs) { + return callback(refs); + }); + }, + selectable: true, + filterable: true, + filterByText: true, + fieldName: 'ref', + renderRow: function(ref) { + var link; + if (ref.header != null) { + return $('<li />').addClass('dropdown-header').text(ref.header); + } else { + link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref)); + return $('<li />').append(link); + } + }, + id: function(obj, $el) { + return $el.attr('data-ref'); + }, + toggleLabel: function(obj, $el) { + return $el.text().trim(); + }, + clicked: function(e) { + return $dropdown.closest('form').submit(); + } + }); + }); + }; + + return Project; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee deleted file mode 100644 index 3288c801388..00000000000 --- a/app/assets/javascripts/project.js.coffee +++ /dev/null @@ -1,91 +0,0 @@ -class @Project - constructor: -> - # Git protocol switcher - $('ul.clone-options-dropdown a').click -> - return if $(@).hasClass('active') - - - # Remove the active class for all buttons (ssh, http, kerberos if shown) - $('.active').not($(@)).removeClass('active'); - # Add the active class for the clicked button - $(@).toggleClass('active') - - url = $("#project_clone").val() - - # Update the input field - $('#project_clone').val(url) - - # Update the command line instructions - $('.clone').text(url) - - # Ref switcher - @initRefSwitcher() - $('.project-refs-select').on 'change', -> - $(@).parents('form').submit() - - $('.hide-no-ssh-message').on 'click', (e) -> - path = '/' - $.cookie('hide_no_ssh_message', 'false', { path: path }) - $(@).parents('.no-ssh-key-message').remove() - e.preventDefault() - - $('.hide-no-password-message').on 'click', (e) -> - path = '/' - $.cookie('hide_no_password_message', 'false', { path: path }) - $(@).parents('.no-password-message').remove() - e.preventDefault() - - @projectSelectDropdown() - - projectSelectDropdown: -> - new ProjectSelect() - - $('.project-item-select').on 'click', (e) => - @changeProject $(e.currentTarget).val() - - $('.js-projects-dropdown-toggle').on 'click', (e) -> - e.preventDefault() - - $('.js-projects-dropdown').select2('open') - - changeProject: (url) -> - window.location = url - - initRefSwitcher: -> - $('.js-project-refs-dropdown').each -> - $dropdown = $(@) - selected = $dropdown.data('selected') - - $dropdown.glDropdown( - data: (term, callback) -> - $.ajax( - url: $dropdown.data('refs-url') - data: - ref: $dropdown.data('ref') - ).done (refs) -> - callback(refs) - selectable: true - filterable: true - filterByText: true - fieldName: 'ref' - renderRow: (ref) -> - if ref.header? - $('<li />') - .addClass('dropdown-header') - .text(ref.header) - else - link = $('<a />') - .attr('href', '#') - .addClass(if ref is selected then 'is-active' else '') - .text(ref) - .attr('data-ref', escape(ref)) - - $('<li />') - .append(link) - id: (obj, $el) -> - $el.attr('data-ref') - toggleLabel: (obj, $el) -> - $el.text().trim() - clicked: (e) -> - $dropdown.closest('form').submit() - ) diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js new file mode 100644 index 00000000000..277e71523d5 --- /dev/null +++ b/app/assets/javascripts/project_avatar.js @@ -0,0 +1,21 @@ +(function() { + this.ProjectAvatar = (function() { + function ProjectAvatar() { + $('.js-choose-project-avatar-button').bind('click', function() { + var form; + form = $(this).closest('form'); + return form.find('.js-project-avatar-input').click(); + }); + $('.js-project-avatar-input').bind('change', function() { + var filename, form; + form = $(this).closest('form'); + filename = $(this).val().replace(/^.*[\\\/]/, ''); + return form.find('.js-avatar-filename').text(filename); + }); + } + + return ProjectAvatar; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/project_avatar.js.coffee b/app/assets/javascripts/project_avatar.js.coffee deleted file mode 100644 index 8bec6e2ccca..00000000000 --- a/app/assets/javascripts/project_avatar.js.coffee +++ /dev/null @@ -1,9 +0,0 @@ -class @ProjectAvatar - constructor: -> - $('.js-choose-project-avatar-button').bind 'click', -> - form = $(this).closest('form') - form.find('.js-project-avatar-input').click() - $('.js-project-avatar-input').bind 'change', -> - form = $(this).closest('form') - filename = $(this).val().replace(/^.*[\\\/]/, '') - form.find('.js-avatar-filename').text(filename) diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js new file mode 100644 index 00000000000..4925f0519f0 --- /dev/null +++ b/app/assets/javascripts/project_find_file.js @@ -0,0 +1,170 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.ProjectFindFile = (function() { + var highlighter; + + function ProjectFindFile(element1, options) { + this.element = element1; + this.options = options; + this.goToBlob = bind(this.goToBlob, this); + this.goToTree = bind(this.goToTree, this); + this.selectRowDown = bind(this.selectRowDown, this); + this.selectRowUp = bind(this.selectRowUp, this); + this.filePaths = {}; + this.inputElement = this.element.find(".file-finder-input"); + this.initEvent(); + this.inputElement.focus(); + this.load(this.options.url); + } + + ProjectFindFile.prototype.initEvent = function() { + this.inputElement.off("keyup"); + this.inputElement.on("keyup", (function(_this) { + return function(event) { + var oldValue, ref, target, value; + target = $(event.target); + value = target.val(); + oldValue = (ref = target.data("oldValue")) != null ? ref : ""; + if (value !== oldValue) { + target.data("oldValue", value); + _this.findFile(); + return _this.element.find("tr.tree-item").eq(0).addClass("selected").focus(); + } + }; + })(this)); + return this.element.find(".tree-content-holder .tree-table").on("click", function(event) { + var path; + if (event.target.nodeName !== "A") { + path = this.element.find(".tree-item-file-name a", this).attr("href"); + if (path) { + return location.href = path; + } + } + }); + }; + + ProjectFindFile.prototype.findFile = function() { + var result, searchText; + searchText = this.inputElement.val(); + result = searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths; + return this.renderList(result, searchText); + }; + + ProjectFindFile.prototype.load = function(url) { + return $.ajax({ + url: url, + method: "get", + dataType: "json", + success: (function(_this) { + return function(data) { + _this.element.find(".loading").hide(); + _this.filePaths = data; + _this.findFile(); + return _this.element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus(); + }; + })(this) + }); + }; + + ProjectFindFile.prototype.renderList = function(filePaths, searchText) { + var blobItemUrl, filePath, html, i, j, len, matches, results; + this.element.find(".tree-table > tbody").empty(); + results = []; + for (i = j = 0, len = filePaths.length; j < len; i = ++j) { + filePath = filePaths[i]; + if (i === 20) { + break; + } + if (searchText) { + matches = fuzzaldrinPlus.match(filePath, searchText); + } + blobItemUrl = this.options.blobUrlTemplate + "/" + filePath; + html = this.makeHtml(filePath, matches, blobItemUrl); + results.push(this.element.find(".tree-table > tbody").append(html)); + } + return results; + }; + + highlighter = function(element, text, matches) { + var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched; + lastIndex = 0; + highlightText = ""; + matchedChars = []; + for (j = 0, len = matches.length; j < len; j++) { + matchIndex = matches[j]; + unmatched = text.substring(lastIndex, matchIndex); + if (unmatched) { + if (matchedChars.length) { + element.append(matchedChars.join("").bold()); + } + matchedChars = []; + element.append(document.createTextNode(unmatched)); + } + matchedChars.push(text[matchIndex]); + lastIndex = matchIndex + 1; + } + if (matchedChars.length) { + element.append(matchedChars.join("").bold()); + } + return element.append(document.createTextNode(text.substring(lastIndex))); + }; + + ProjectFindFile.prototype.makeHtml = function(filePath, matches, blobItemUrl) { + var $tr; + $tr = $("<tr class='tree-item'><td class='tree-item-file-name'><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'><a></a></span></td></tr>"); + if (matches) { + $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl)); + } else { + $tr.find("a").attr("href", blobItemUrl).text(filePath); + } + return $tr; + }; + + ProjectFindFile.prototype.selectRow = function(type) { + var next, rows, selectedRow; + rows = this.element.find(".files-slider tr.tree-item"); + selectedRow = this.element.find(".files-slider tr.tree-item.selected"); + if (rows && rows.length > 0) { + if (selectedRow && selectedRow.length > 0) { + if (type === "UP") { + next = selectedRow.prev(); + } else if (type === "DOWN") { + next = selectedRow.next(); + } + if (next.length > 0) { + selectedRow.removeClass("selected"); + selectedRow = next; + } + } else { + selectedRow = rows.eq(0); + } + return selectedRow.addClass("selected").focus(); + } + }; + + ProjectFindFile.prototype.selectRowUp = function() { + return this.selectRow("UP"); + }; + + ProjectFindFile.prototype.selectRowDown = function() { + return this.selectRow("DOWN"); + }; + + ProjectFindFile.prototype.goToTree = function() { + return location.href = this.options.treeUrl; + }; + + ProjectFindFile.prototype.goToBlob = function() { + var path; + path = this.element.find(".tree-item.selected .tree-item-file-name a").attr("href"); + if (path) { + return location.href = path; + } + }; + + return ProjectFindFile; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/project_find_file.js.coffee b/app/assets/javascripts/project_find_file.js.coffee deleted file mode 100644 index 0dd32352c34..00000000000 --- a/app/assets/javascripts/project_find_file.js.coffee +++ /dev/null @@ -1,125 +0,0 @@ -class @ProjectFindFile
- constructor: (@element, @options)->
- @filePaths = {}
- @inputElement = @element.find(".file-finder-input")
-
- # init event
- @initEvent()
-
- # focus text input box
- @inputElement.focus()
-
- # load file list
- @load(@options.url)
-
- # init event
- initEvent: ->
- @inputElement.off "keyup"
- @inputElement.on "keyup", (event) =>
- target = $(event.target)
- value = target.val()
- oldValue = target.data("oldValue") ? ""
-
- if value != oldValue
- target.data("oldValue", value)
- @findFile()
- @element.find("tr.tree-item").eq(0).addClass("selected").focus()
-
- @element.find(".tree-content-holder .tree-table").on "click", (event) ->
- if (event.target.nodeName != "A")
- path = @element.find(".tree-item-file-name a", this).attr("href")
- location.href = path if path
-
- # find file
- findFile: ->
- searchText = @inputElement.val()
- result = if searchText.length > 0 then fuzzaldrinPlus.filter(@filePaths, searchText) else @filePaths
- @renderList result, searchText
-
- # files pathes load
- load: (url) ->
- $.ajax
- url: url
- method: "get"
- dataType: "json"
- success: (data) =>
- @element.find(".loading").hide()
- @filePaths = data
- @findFile()
- @element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus()
-
- # render result
- renderList: (filePaths, searchText) ->
- @element.find(".tree-table > tbody").empty()
-
- for filePath, i in filePaths
- break if i == 20
-
- if searchText
- matches = fuzzaldrinPlus.match(filePath, searchText)
-
- blobItemUrl = "#{@options.blobUrlTemplate}/#{filePath}"
-
- html = @makeHtml filePath, matches, blobItemUrl
- @element.find(".tree-table > tbody").append(html)
-
- # highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
- highlighter = (element, text, matches) ->
- lastIndex = 0
- highlightText = ""
- matchedChars = []
-
- for matchIndex in matches
- unmatched = text.substring(lastIndex, matchIndex)
-
- if unmatched
- element.append(matchedChars.join("").bold()) if matchedChars.length
- matchedChars = []
- element.append(document.createTextNode(unmatched))
-
- matchedChars.push(text[matchIndex])
- lastIndex = matchIndex + 1
-
- element.append(matchedChars.join("").bold()) if matchedChars.length
- element.append(document.createTextNode(text.substring(lastIndex)))
-
- # make tbody row html
- makeHtml: (filePath, matches, blobItemUrl) ->
- $tr = $("<tr class='tree-item'><td class='tree-item-file-name'><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'><a></a></span></td></tr>")
- if matches
- $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl))
- else
- $tr.find("a").attr("href", blobItemUrl).text(filePath)
-
- return $tr
-
- selectRow: (type) ->
- rows = @element.find(".files-slider tr.tree-item")
- selectedRow = @element.find(".files-slider tr.tree-item.selected")
-
- if rows && rows.length > 0
- if selectedRow && selectedRow.length > 0
- if type == "UP"
- next = selectedRow.prev()
- else if type == "DOWN"
- next = selectedRow.next()
-
- if next.length > 0
- selectedRow.removeClass "selected"
- selectedRow = next
- else
- selectedRow = rows.eq(0)
- selectedRow.addClass("selected").focus()
-
- selectRowUp: =>
- @selectRow "UP"
-
- selectRowDown: =>
- @selectRow "DOWN"
-
- goToTree: =>
- location.href = @options.treeUrl
-
- goToBlob: =>
- path = @element.find(".tree-item.selected .tree-item-file-name a").attr("href")
- location.href = path if path
diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js new file mode 100644 index 00000000000..d2261c51f35 --- /dev/null +++ b/app/assets/javascripts/project_fork.js @@ -0,0 +1,14 @@ +(function() { + this.ProjectFork = (function() { + function ProjectFork() { + $('.fork-thumbnail a').on('click', function() { + $('.fork-namespaces').hide(); + return $('.save-project-loader').show(); + }); + } + + return ProjectFork; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/project_fork.js.coffee b/app/assets/javascripts/project_fork.js.coffee deleted file mode 100644 index e15a1c4ef76..00000000000 --- a/app/assets/javascripts/project_fork.js.coffee +++ /dev/null @@ -1,5 +0,0 @@ -class @ProjectFork - constructor: -> - $('.fork-thumbnail a').on 'click', -> - $('.fork-namespaces').hide() - $('.save-project-loader').show() diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js new file mode 100644 index 00000000000..c61b0cf2fde --- /dev/null +++ b/app/assets/javascripts/project_import.js @@ -0,0 +1,13 @@ +(function() { + this.ProjectImport = (function() { + function ProjectImport() { + setTimeout(function() { + return Turbolinks.visit(location.href); + }, 5000); + } + + return ProjectImport; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/project_import.js.coffee b/app/assets/javascripts/project_import.js.coffee deleted file mode 100644 index 6633564a079..00000000000 --- a/app/assets/javascripts/project_import.js.coffee +++ /dev/null @@ -1,5 +0,0 @@ -class @ProjectImport - constructor: -> - setTimeout -> - Turbolinks.visit(location.href) - , 5000 diff --git a/app/assets/javascripts/project_members.js b/app/assets/javascripts/project_members.js new file mode 100644 index 00000000000..f6a796b325a --- /dev/null +++ b/app/assets/javascripts/project_members.js @@ -0,0 +1,13 @@ +(function() { + this.ProjectMembers = (function() { + function ProjectMembers() { + $('li.project_member').bind('ajax:success', function() { + return $(this).fadeOut(); + }); + } + + return ProjectMembers; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/project_members.js.coffee b/app/assets/javascripts/project_members.js.coffee deleted file mode 100644 index 896ba7e53ee..00000000000 --- a/app/assets/javascripts/project_members.js.coffee +++ /dev/null @@ -1,4 +0,0 @@ -class @ProjectMembers - constructor: -> - $('li.project_member').bind 'ajax:success', -> - $(this).fadeOut() diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js new file mode 100644 index 00000000000..798f15e40a0 --- /dev/null +++ b/app/assets/javascripts/project_new.js @@ -0,0 +1,40 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.ProjectNew = (function() { + function ProjectNew() { + this.toggleSettings = bind(this.toggleSettings, this); + $('.project-edit-container').on('ajax:before', (function(_this) { + return function() { + $('.project-edit-container').hide(); + return $('.save-project-loader').show(); + }; + })(this)); + this.toggleSettings(); + this.toggleSettingsOnclick(); + } + + ProjectNew.prototype.toggleSettings = function() { + this._showOrHide('#project_builds_enabled', '.builds-feature'); + return this._showOrHide('#project_merge_requests_enabled', '.merge-requests-feature'); + }; + + ProjectNew.prototype.toggleSettingsOnclick = function() { + return $('#project_builds_enabled, #project_merge_requests_enabled').on('click', this.toggleSettings); + }; + + ProjectNew.prototype._showOrHide = function(checkElement, container) { + var $container; + $container = $(container); + if ($(checkElement).prop('checked')) { + return $container.show(); + } else { + return $container.hide(); + } + }; + + return ProjectNew; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/project_new.js.coffee b/app/assets/javascripts/project_new.js.coffee deleted file mode 100644 index e48343a19b5..00000000000 --- a/app/assets/javascripts/project_new.js.coffee +++ /dev/null @@ -1,23 +0,0 @@ -class @ProjectNew - constructor: -> - $('.project-edit-container').on 'ajax:before', => - $('.project-edit-container').hide() - $('.save-project-loader').show() - @toggleSettings() - @toggleSettingsOnclick() - - - toggleSettings: => - @_showOrHide('#project_builds_enabled', '.builds-feature') - @_showOrHide('#project_merge_requests_enabled', '.merge-requests-feature') - - toggleSettingsOnclick: -> - $('#project_builds_enabled, #project_merge_requests_enabled').on 'click', @toggleSettings - - _showOrHide: (checkElement, container) -> - $container = $(container) - - if $(checkElement).prop('checked') - $container.show() - else - $container.hide() diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js new file mode 100644 index 00000000000..20b147500cf --- /dev/null +++ b/app/assets/javascripts/project_select.js @@ -0,0 +1,102 @@ +(function() { + this.ProjectSelect = (function() { + function ProjectSelect() { + $('.js-projects-dropdown-toggle').each(function(i, dropdown) { + var $dropdown; + $dropdown = $(dropdown); + return $dropdown.glDropdown({ + filterable: true, + filterRemote: true, + search: { + fields: ['name_with_namespace'] + }, + data: function(term, callback) { + var finalCallback, projectsCallback; + finalCallback = function(projects) { + return callback(projects); + }; + if (this.includeGroups) { + projectsCallback = function(projects) { + var groupsCallback; + groupsCallback = function(groups) { + var data; + data = groups.concat(projects); + return finalCallback(data); + }; + return Api.groups(term, false, groupsCallback); + }; + } else { + projectsCallback = finalCallback; + } + if (this.groupId) { + return Api.groupProjects(this.groupId, term, projectsCallback); + } else { + return Api.projects(term, this.orderBy, projectsCallback); + } + }, + url: function(project) { + return project.web_url; + }, + text: function(project) { + return project.name_with_namespace; + } + }); + }); + $('.ajax-project-select').each(function(i, select) { + var placeholder; + this.groupId = $(select).data('group-id'); + this.includeGroups = $(select).data('include-groups'); + this.orderBy = $(select).data('order-by') || 'id'; + placeholder = "Search for project"; + if (this.includeGroups) { + placeholder += " or group"; + } + return $(select).select2({ + placeholder: placeholder, + minimumInputLength: 0, + query: (function(_this) { + return function(query) { + var finalCallback, projectsCallback; + finalCallback = function(projects) { + var data; + data = { + results: projects + }; + return query.callback(data); + }; + if (_this.includeGroups) { + projectsCallback = function(projects) { + var groupsCallback; + groupsCallback = function(groups) { + var data; + data = groups.concat(projects); + return finalCallback(data); + }; + return Api.groups(query.term, false, groupsCallback); + }; + } else { + projectsCallback = finalCallback; + } + if (_this.groupId) { + return Api.groupProjects(_this.groupId, query.term, projectsCallback); + } else { + return Api.projects(query.term, _this.orderBy, projectsCallback); + } + }; + })(this), + id: function(project) { + return project.web_url; + }, + text: function(project) { + return project.name_with_namespace || project.name; + }, + dropdownCssClass: "ajax-project-dropdown" + }); + }); + } + + return ProjectSelect; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee deleted file mode 100644 index 704bd8dee53..00000000000 --- a/app/assets/javascripts/project_select.js.coffee +++ /dev/null @@ -1,72 +0,0 @@ -class @ProjectSelect - constructor: -> - $('.js-projects-dropdown-toggle').each (i, dropdown) -> - $dropdown = $(dropdown) - - $dropdown.glDropdown( - filterable: true - filterRemote: true - search: - fields: ['name_with_namespace'] - data: (term, callback) -> - finalCallback = (projects) -> - callback projects - - if @includeGroups - projectsCallback = (projects) -> - groupsCallback = (groups) -> - data = groups.concat(projects) - finalCallback(data) - - Api.groups term, false, groupsCallback - else - projectsCallback = finalCallback - - if @groupId - Api.groupProjects @groupId, term, projectsCallback - else - Api.projects term, @orderBy, projectsCallback - url: (project) -> - project.web_url - text: (project) -> - project.name_with_namespace - ) - - $('.ajax-project-select').each (i, select) -> - @groupId = $(select).data('group-id') - @includeGroups = $(select).data('include-groups') - @orderBy = $(select).data('order-by') || 'id' - - placeholder = "Search for project" - placeholder += " or group" if @includeGroups - - $(select).select2 - placeholder: placeholder - minimumInputLength: 0 - query: (query) => - finalCallback = (projects) -> - data = { results: projects } - query.callback(data) - - if @includeGroups - projectsCallback = (projects) -> - groupsCallback = (groups) -> - data = groups.concat(projects) - finalCallback(data) - - Api.groups query.term, false, groupsCallback - else - projectsCallback = finalCallback - - if @groupId - Api.groupProjects @groupId, query.term, projectsCallback - else - Api.projects query.term, @orderBy, projectsCallback - - id: (project) -> - project.web_url - - text: (project) -> - project.name_with_namespace || project.name - - dropdownCssClass: "ajax-project-dropdown" diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js new file mode 100644 index 00000000000..8ca4c427912 --- /dev/null +++ b/app/assets/javascripts/project_show.js @@ -0,0 +1,9 @@ +(function() { + this.ProjectShow = (function() { + function ProjectShow() {} + + return ProjectShow; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/project_show.js.coffee b/app/assets/javascripts/project_show.js.coffee deleted file mode 100644 index 1fdf28f2528..00000000000 --- a/app/assets/javascripts/project_show.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -class @ProjectShow - constructor: -> - # I kept class for future diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js new file mode 100644 index 00000000000..4f415b05dbc --- /dev/null +++ b/app/assets/javascripts/projects_list.js @@ -0,0 +1,48 @@ +(function() { + this.ProjectsList = { + init: function() { + $(".projects-list-filter").off('keyup'); + this.initSearch(); + return this.initPagination(); + }, + initSearch: function() { + var debounceFilter, projectsListFilter; + projectsListFilter = $('.projects-list-filter'); + debounceFilter = _.debounce(ProjectsList.filterResults, 500); + return projectsListFilter.on('keyup', function(e) { + if (projectsListFilter.val() !== '') { + return debounceFilter(); + } + }); + }, + filterResults: function() { + var form, project_filter_url, search; + $('.projects-list-holder').fadeTo(250, 0.5); + form = null; + form = $("form#project-filter-form"); + search = $(".projects-list-filter").val(); + project_filter_url = form.attr('action') + '?' + form.serialize(); + return $.ajax({ + type: "GET", + url: form.attr('action'), + data: form.serialize(), + complete: function() { + return $('.projects-list-holder').fadeTo(250, 1); + }, + success: function(data) { + $('.projects-list-holder').replaceWith(data.html); + return history.replaceState({ + page: project_filter_url + }, document.title, project_filter_url); + }, + dataType: "json" + }); + }, + initPagination: function() { + return $('.projects-list-holder .pagination').on('ajax:success', function(e, data) { + return $('.projects-list-holder').replaceWith(data.html); + }); + } + }; + +}).call(this); diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee deleted file mode 100644 index a7d78d9e461..00000000000 --- a/app/assets/javascripts/projects_list.js.coffee +++ /dev/null @@ -1,36 +0,0 @@ -@ProjectsList = - init: -> - $(".projects-list-filter").off('keyup') - this.initSearch() - this.initPagination() - - initSearch: -> - projectsListFilter = $('.projects-list-filter') - debounceFilter = _.debounce ProjectsList.filterResults, 500 - projectsListFilter.on 'keyup', (e) -> - debounceFilter() if projectsListFilter.val() isnt '' - - filterResults: -> - $('.projects-list-holder').fadeTo(250, 0.5) - - form = null - form = $("form#project-filter-form") - search = $(".projects-list-filter").val() - project_filter_url = form.attr('action') + '?' + form.serialize() - - $.ajax - type: "GET" - url: form.attr('action') - data: form.serialize() - complete: -> - $('.projects-list-holder').fadeTo(250, 1) - success: (data) -> - $('.projects-list-holder').replaceWith(data.html) - # Change url so if user reload a page - search results are saved - history.replaceState {page: project_filter_url}, document.title, project_filter_url - dataType: "json" - - initPagination: -> - $('.projects-list-holder .pagination').on('ajax:success', (e, data) -> - $('.projects-list-holder').replaceWith(data.html) - ) diff --git a/app/assets/javascripts/protected_branch_select.js b/app/assets/javascripts/protected_branch_select.js new file mode 100644 index 00000000000..3a47fc972dc --- /dev/null +++ b/app/assets/javascripts/protected_branch_select.js @@ -0,0 +1,72 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.ProtectedBranchSelect = (function() { + function ProtectedBranchSelect(currentProject) { + this.toggleCreateNewButton = bind(this.toggleCreateNewButton, this); + this.getProtectedBranches = bind(this.getProtectedBranches, this); + $('.dropdown-footer').hide(); + this.dropdown = $('.js-protected-branch-select').glDropdown({ + data: this.getProtectedBranches, + filterable: true, + remote: false, + search: { + fields: ['title'] + }, + selectable: true, + toggleLabel: function(selected) { + if (selected && 'id' in selected) { + return selected.title; + } else { + return 'Protected Branch'; + } + }, + fieldName: 'protected_branch[name]', + text: function(protected_branch) { + return _.escape(protected_branch.title); + }, + id: function(protected_branch) { + return _.escape(protected_branch.id); + }, + onFilter: this.toggleCreateNewButton, + clicked: function() { + return $('.protect-branch-btn').attr('disabled', false); + } + }); + $('.create-new-protected-branch').on('click', (function(_this) { + return function(event) { + _this.dropdown.data('glDropdown').remote.execute(); + return _this.dropdown.data('glDropdown').selectRowAtIndex(event, 0); + }; + })(this)); + } + + ProtectedBranchSelect.prototype.getProtectedBranches = function(term, callback) { + if (this.selectedBranch) { + return callback(gon.open_branches.concat(this.selectedBranch)); + } else { + return callback(gon.open_branches); + } + }; + + ProtectedBranchSelect.prototype.toggleCreateNewButton = function(branchName) { + this.selectedBranch = { + title: branchName, + id: branchName, + text: branchName + }; + if (branchName === '') { + $('.protected-branch-select-footer-list').addClass('hidden'); + return $('.dropdown-footer').hide(); + } else { + $('.create-new-protected-branch').text("Create Protected Branch: " + branchName); + $('.protected-branch-select-footer-list').removeClass('hidden'); + return $('.dropdown-footer').show(); + } + }; + + return ProtectedBranchSelect; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/protected_branch_select.js.coffee b/app/assets/javascripts/protected_branch_select.js.coffee deleted file mode 100644 index 6d45770ace9..00000000000 --- a/app/assets/javascripts/protected_branch_select.js.coffee +++ /dev/null @@ -1,40 +0,0 @@ -class @ProtectedBranchSelect - constructor: (currentProject) -> - $('.dropdown-footer').hide(); - @dropdown = $('.js-protected-branch-select').glDropdown( - data: @getProtectedBranches - filterable: true - remote: false - search: - fields: ['title'] - selectable: true - toggleLabel: (selected) -> if (selected and 'id' of selected) then selected.title else 'Protected Branch' - fieldName: 'protected_branch[name]' - text: (protected_branch) -> _.escape(protected_branch.title) - id: (protected_branch) -> _.escape(protected_branch.id) - onFilter: @toggleCreateNewButton - clicked: () -> $('.protect-branch-btn').attr('disabled', false) - ) - - $('.create-new-protected-branch').on 'click', (event) => - # Refresh the dropdown's data, which ends up calling `getProtectedBranches` - @dropdown.data('glDropdown').remote.execute() - @dropdown.data('glDropdown').selectRowAtIndex(event, 0) - - getProtectedBranches: (term, callback) => - if @selectedBranch - callback(gon.open_branches.concat(@selectedBranch)) - else - callback(gon.open_branches) - - toggleCreateNewButton: (branchName) => - @selectedBranch = { title: branchName, id: branchName, text: branchName } - - if branchName is '' - $('.protected-branch-select-footer-list').addClass('hidden') - $('.dropdown-footer').hide(); - else - $('.create-new-protected-branch').text("Create Protected Branch: #{branchName}") - $('.protected-branch-select-footer-list').removeClass('hidden') - $('.dropdown-footer').show(); - diff --git a/app/assets/javascripts/protected_branches.js b/app/assets/javascripts/protected_branches.js new file mode 100644 index 00000000000..db21a19964d --- /dev/null +++ b/app/assets/javascripts/protected_branches.js @@ -0,0 +1,35 @@ +(function() { + $(function() { + return $(".protected-branches-list :checkbox").change(function(e) { + var can_push, id, name, obj, url; + name = $(this).attr("name"); + if (name === "developers_can_push" || name === "developers_can_merge") { + id = $(this).val(); + can_push = $(this).is(":checked"); + url = $(this).data("url"); + return $.ajax({ + type: "PATCH", + url: url, + dataType: "json", + data: { + id: id, + protected_branch: ( + obj = {}, + obj["" + name] = can_push, + obj + ) + }, + success: function() { + var row; + row = $(e.target); + return row.closest('tr').effect('highlight'); + }, + error: function() { + return new Flash("Failed to update branch!", "alert"); + } + }); + } + }); + }); + +}).call(this); diff --git a/app/assets/javascripts/protected_branches.js.coffee b/app/assets/javascripts/protected_branches.js.coffee deleted file mode 100644 index 14afef2e2ee..00000000000 --- a/app/assets/javascripts/protected_branches.js.coffee +++ /dev/null @@ -1,22 +0,0 @@ -$ -> - $(".protected-branches-list :checkbox").change (e) -> - name = $(this).attr("name") - if name == "developers_can_push" || name == "developers_can_merge" - id = $(this).val() - can_push = $(this).is(":checked") - url = $(this).data("url") - $.ajax - type: "PATCH" - url: url - dataType: "json" - data: - id: id - protected_branch: - "#{name}": can_push - - success: -> - row = $(e.target) - row.closest('tr').effect('highlight') - - error: -> - new Flash("Failed to update branch!", "alert") diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js new file mode 100644 index 00000000000..dc4d5113826 --- /dev/null +++ b/app/assets/javascripts/right_sidebar.js @@ -0,0 +1,201 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Sidebar = (function() { + function Sidebar(currentUser) { + this.toggleTodo = bind(this.toggleTodo, this); + this.sidebar = $('aside'); + this.addEventListeners(); + } + + Sidebar.prototype.addEventListeners = function() { + 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).off('click', '.js-sidebar-toggle').on('click', '.js-sidebar-toggle', function(e, triggered) { + var $allGutterToggleIcons, $this, $thisIcon; + e.preventDefault(); + $this = $(this); + $thisIcon = $this.find('i'); + $allGutterToggleIcons = $('.js-sidebar-toggle i'); + if ($thisIcon.hasClass('fa-angle-double-right')) { + $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left'); + $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); + $('.page-with-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); + } else { + $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right'); + $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); + $('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); + } + if (!triggered) { + return $.cookie("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'), { + path: '/' + }); + } + }); + return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo); + }; + + Sidebar.prototype.toggleTodo = function(e) { + var $btnText, $this, $todoLoading, ajaxType, url; + $this = $(e.currentTarget); + $todoLoading = $('.js-issuable-todo-loading'); + $btnText = $('.js-issuable-todo-text', $this); + ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST'; + if ($this.attr('data-delete-path')) { + url = "" + ($this.attr('data-delete-path')); + } else { + url = "" + ($this.data('url')); + } + return $.ajax({ + url: url, + type: ajaxType, + dataType: 'json', + data: { + issuable_id: $this.data('issuable-id'), + issuable_type: $this.data('issuable-type') + }, + beforeSend: (function(_this) { + return function() { + return _this.beforeTodoSend($this, $todoLoading); + }; + })(this) + }).done((function(_this) { + return function(data) { + return _this.todoUpdateDone(data, $this, $btnText, $todoLoading); + }; + })(this)); + }; + + Sidebar.prototype.beforeTodoSend = function($btn, $todoLoading) { + $btn.disable(); + return $todoLoading.removeClass('hidden'); + }; + + Sidebar.prototype.todoUpdateDone = function(data, $btn, $btnText, $todoLoading) { + var $todoPendingCount; + $todoPendingCount = $('.todos-pending-count'); + $todoPendingCount.text(data.count); + $btn.enable(); + $todoLoading.addClass('hidden'); + if (data.count === 0) { + $todoPendingCount.addClass('hidden'); + } else { + $todoPendingCount.removeClass('hidden'); + } + if (data.delete_path != null) { + $btn.attr('aria-label', $btn.data('mark-text')).attr('data-delete-path', data.delete_path); + return $btnText.text($btn.data('mark-text')); + } else { + $btn.attr('aria-label', $btn.data('todo-text')).removeAttr('data-delete-path'); + return $btnText.text($btn.data('todo-text')); + } + }; + + Sidebar.prototype.sidebarDropdownLoading = function(e) { + var $loading, $sidebarCollapsedIcon, i, img; + $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon'); + img = $sidebarCollapsedIcon.find('img'); + i = $sidebarCollapsedIcon.find('i'); + $loading = $('<i class="fa fa-spinner fa-spin"></i>'); + if (img.length) { + img.before($loading); + return img.hide(); + } else if (i.length) { + i.before($loading); + return i.hide(); + } + }; + + Sidebar.prototype.sidebarDropdownLoaded = function(e) { + var $sidebarCollapsedIcon, i, img; + $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon'); + img = $sidebarCollapsedIcon.find('img'); + $sidebarCollapsedIcon.find('i.fa-spin').remove(); + i = $sidebarCollapsedIcon.find('i'); + if (img.length) { + return img.show(); + } else { + return i.show(); + } + }; + + Sidebar.prototype.sidebarCollapseClicked = function(e) { + var $block, sidebar; + if ($(e.currentTarget).hasClass('dont-change-state')) { + return; + } + sidebar = e.data; + e.preventDefault(); + $block = $(this).closest('.block'); + return sidebar.openDropdown($block); + }; + + Sidebar.prototype.openDropdown = function(blockOrName) { + var $block; + $block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName; + $block.find('.edit-link').trigger('click'); + if (!this.isOpen()) { + this.setCollapseAfterUpdate($block); + return this.toggleSidebar('open'); + } + }; + + Sidebar.prototype.setCollapseAfterUpdate = function($block) { + $block.addClass('collapse-after-update'); + return $('.page-with-sidebar').addClass('with-overlay'); + }; + + Sidebar.prototype.onSidebarDropdownHidden = function(e) { + var $block, sidebar; + sidebar = e.data; + e.preventDefault(); + $block = $(this).closest('.block'); + return sidebar.sidebarDropdownHidden($block); + }; + + Sidebar.prototype.sidebarDropdownHidden = function($block) { + if ($block.hasClass('collapse-after-update')) { + $block.removeClass('collapse-after-update'); + $('.page-with-sidebar').removeClass('with-overlay'); + return this.toggleSidebar('hide'); + } + }; + + Sidebar.prototype.triggerOpenSidebar = function() { + return this.sidebar.find('.js-sidebar-toggle').trigger('click'); + }; + + Sidebar.prototype.toggleSidebar = function(action) { + if (action == null) { + action = 'toggle'; + } + if (action === 'toggle') { + this.triggerOpenSidebar(); + } + if (action === 'open') { + if (!this.isOpen()) { + this.triggerOpenSidebar(); + } + } + if (action === 'hide') { + if (this.isOpen()) { + return this.triggerOpenSidebar(); + } + } + }; + + Sidebar.prototype.isOpen = function() { + return this.sidebar.is('.right-sidebar-expanded'); + }; + + Sidebar.prototype.getBlock = function(name) { + return this.sidebar.find(".block." + name); + }; + + return Sidebar; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/right_sidebar.js.coffee b/app/assets/javascripts/right_sidebar.js.coffee deleted file mode 100644 index 0c95301e380..00000000000 --- a/app/assets/javascripts/right_sidebar.js.coffee +++ /dev/null @@ -1,175 +0,0 @@ -class @Sidebar - constructor: (currentUser) -> - @sidebar = $('aside') - - @addEventListeners() - - addEventListeners: -> - @sidebar.on('click', '.sidebar-collapsed-icon', @, @sidebarCollapseClicked) - $('.dropdown').on('hidden.gl.dropdown', @, @onSidebarDropdownHidden) - $('.dropdown').on('loading.gl.dropdown', @sidebarDropdownLoading) - $('.dropdown').on('loaded.gl.dropdown', @sidebarDropdownLoaded) - - - $(document) - .off 'click', '.js-sidebar-toggle' - .on 'click', '.js-sidebar-toggle', (e, triggered) -> - e.preventDefault() - $this = $(this) - $thisIcon = $this.find 'i' - $allGutterToggleIcons = $('.js-sidebar-toggle i') - if $thisIcon.hasClass('fa-angle-double-right') - $allGutterToggleIcons - .removeClass('fa-angle-double-right') - .addClass('fa-angle-double-left') - $('aside.right-sidebar') - .removeClass('right-sidebar-expanded') - .addClass('right-sidebar-collapsed') - $('.page-with-sidebar') - .removeClass('right-sidebar-expanded') - .addClass('right-sidebar-collapsed') - else - $allGutterToggleIcons - .removeClass('fa-angle-double-left') - .addClass('fa-angle-double-right') - $('aside.right-sidebar') - .removeClass('right-sidebar-collapsed') - .addClass('right-sidebar-expanded') - $('.page-with-sidebar') - .removeClass('right-sidebar-collapsed') - .addClass('right-sidebar-expanded') - if not triggered - $.cookie("collapsed_gutter", - $('.right-sidebar') - .hasClass('right-sidebar-collapsed'), { path: '/' }) - - $(document) - .off 'click', '.js-issuable-todo' - .on 'click', '.js-issuable-todo', @toggleTodo - - toggleTodo: (e) => - $this = $(e.currentTarget) - $todoLoading = $('.js-issuable-todo-loading') - $btnText = $('.js-issuable-todo-text', $this) - ajaxType = if $this.attr('data-delete-path') then 'DELETE' else 'POST' - - if $this.attr('data-delete-path') - url = "#{$this.attr('data-delete-path')}" - else - url = "#{$this.data('url')}" - - $.ajax( - url: url - type: ajaxType - dataType: 'json' - data: - issuable_id: $this.data('issuable-id') - issuable_type: $this.data('issuable-type') - beforeSend: => - @beforeTodoSend($this, $todoLoading) - ).done (data) => - @todoUpdateDone(data, $this, $btnText, $todoLoading) - - beforeTodoSend: ($btn, $todoLoading) -> - $btn.disable() - $todoLoading.removeClass 'hidden' - - todoUpdateDone: (data, $btn, $btnText, $todoLoading) -> - $todoPendingCount = $('.todos-pending-count') - $todoPendingCount.text data.count - - $btn.enable() - $todoLoading.addClass 'hidden' - - if data.count is 0 - $todoPendingCount.addClass 'hidden' - else - $todoPendingCount.removeClass 'hidden' - - if data.delete_path? - $btn - .attr 'aria-label', $btn.data('mark-text') - .attr 'data-delete-path', data.delete_path - $btnText.text $btn.data('mark-text') - else - $btn - .attr 'aria-label', $btn.data('todo-text') - .removeAttr 'data-delete-path' - $btnText.text $btn.data('todo-text') - - sidebarDropdownLoading: (e) -> - $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon') - img = $sidebarCollapsedIcon.find('img') - i = $sidebarCollapsedIcon.find('i') - $loading = $('<i class="fa fa-spinner fa-spin"></i>') - if img.length - img.before($loading) - img.hide() - else if i.length - i.before($loading) - i.hide() - - sidebarDropdownLoaded: (e) -> - $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon') - img = $sidebarCollapsedIcon.find('img') - $sidebarCollapsedIcon.find('i.fa-spin').remove() - i = $sidebarCollapsedIcon.find('i') - if img.length - img.show() - else - i.show() - - sidebarCollapseClicked: (e) -> - - return if $(e.currentTarget).hasClass('dont-change-state') - - sidebar = e.data - e.preventDefault() - $block = $(@).closest('.block') - sidebar.openDropdown($block); - - openDropdown: (blockOrName) -> - $block = if _.isString(blockOrName) then @getBlock(blockOrName) else blockOrName - - $block.find('.edit-link').trigger('click') - - if not @isOpen() - @setCollapseAfterUpdate($block) - @toggleSidebar('open') - - setCollapseAfterUpdate: ($block) -> - $block.addClass('collapse-after-update') - $('.page-with-sidebar').addClass('with-overlay') - - onSidebarDropdownHidden: (e) -> - sidebar = e.data - e.preventDefault() - $block = $(@).closest('.block') - sidebar.sidebarDropdownHidden($block) - - sidebarDropdownHidden: ($block) -> - if $block.hasClass('collapse-after-update') - $block.removeClass('collapse-after-update') - $('.page-with-sidebar').removeClass('with-overlay') - @toggleSidebar('hide') - - triggerOpenSidebar: -> - @sidebar - .find('.js-sidebar-toggle') - .trigger('click') - - toggleSidebar: (action = 'toggle') -> - if action is 'toggle' - @triggerOpenSidebar() - - if action is 'open' - @triggerOpenSidebar() if not @isOpen() - - if action is 'hide' - @triggerOpenSidebar() if @isOpen() - - isOpen: -> - @sidebar.is('.right-sidebar-expanded') - - getBlock: (name) -> - @sidebar.find(".block.#{name}") diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js new file mode 100644 index 00000000000..d34346f862b --- /dev/null +++ b/app/assets/javascripts/search.js @@ -0,0 +1,93 @@ +(function() { + this.Search = (function() { + function Search() { + var $groupDropdown, $projectDropdown; + $groupDropdown = $('.js-search-group-dropdown'); + $projectDropdown = $('.js-search-project-dropdown'); + this.eventListeners(); + $groupDropdown.glDropdown({ + selectable: true, + filterable: true, + fieldName: 'group_id', + data: function(term, callback) { + return Api.groups(term, null, function(data) { + data.unshift({ + name: 'Any' + }); + data.splice(1, 0, 'divider'); + return callback(data); + }); + }, + id: function(obj) { + return obj.id; + }, + text: function(obj) { + return obj.name; + }, + toggleLabel: function(obj) { + return ($groupDropdown.data('default-label')) + " " + obj.name; + }, + clicked: (function(_this) { + return function() { + return _this.submitSearch(); + }; + })(this) + }); + $projectDropdown.glDropdown({ + selectable: true, + filterable: true, + fieldName: 'project_id', + data: function(term, callback) { + return Api.projects(term, 'id', function(data) { + data.unshift({ + name_with_namespace: 'Any' + }); + data.splice(1, 0, 'divider'); + return callback(data); + }); + }, + id: function(obj) { + return obj.id; + }, + text: function(obj) { + return obj.name_with_namespace; + }, + toggleLabel: function(obj) { + return ($projectDropdown.data('default-label')) + " " + obj.name_with_namespace; + }, + clicked: (function(_this) { + return function() { + return _this.submitSearch(); + }; + })(this) + }); + } + + Search.prototype.eventListeners = function() { + $(document).off('keyup', '.js-search-input').on('keyup', '.js-search-input', this.searchKeyUp); + return $(document).off('click', '.js-search-clear').on('click', '.js-search-clear', this.clearSearchField); + }; + + Search.prototype.submitSearch = function() { + return $('.js-search-form').submit(); + }; + + Search.prototype.searchKeyUp = function() { + var $input; + $input = $(this); + if ($input.val() === '') { + return $('.js-search-clear').addClass('hidden'); + } else { + return $('.js-search-clear').removeClass('hidden'); + } + }; + + Search.prototype.clearSearchField = function() { + return $('.js-search-input').val('').trigger('keyup').focus(); + }; + + return Search; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/search.js.coffee b/app/assets/javascripts/search.js.coffee deleted file mode 100644 index 661e1195f60..00000000000 --- a/app/assets/javascripts/search.js.coffee +++ /dev/null @@ -1,75 +0,0 @@ -class @Search - constructor: -> - $groupDropdown = $('.js-search-group-dropdown') - $projectDropdown = $('.js-search-project-dropdown') - @eventListeners() - - $groupDropdown.glDropdown( - selectable: true - filterable: true - fieldName: 'group_id' - data: (term, callback) -> - Api.groups term, null, (data) -> - data.unshift( - name: 'Any' - ) - data.splice 1, 0, 'divider' - - callback(data) - id: (obj) -> - obj.id - text: (obj) -> - obj.name - toggleLabel: (obj) -> - "#{$groupDropdown.data('default-label')} #{obj.name}" - clicked: => - @submitSearch() - ) - - $projectDropdown.glDropdown( - selectable: true - filterable: true - fieldName: 'project_id' - data: (term, callback) -> - Api.projects term, 'id', (data) -> - data.unshift( - name_with_namespace: 'Any' - ) - data.splice 1, 0, 'divider' - - callback(data) - id: (obj) -> - obj.id - text: (obj) -> - obj.name_with_namespace - toggleLabel: (obj) -> - "#{$projectDropdown.data('default-label')} #{obj.name_with_namespace}" - clicked: => - @submitSearch() - ) - - eventListeners: -> - $(document) - .off 'keyup', '.js-search-input' - .on 'keyup', '.js-search-input', @searchKeyUp - - $(document) - .off 'click', '.js-search-clear' - .on 'click', '.js-search-clear', @clearSearchField - - submitSearch: -> - $('.js-search-form').submit() - - searchKeyUp: -> - $input = $(@) - - if $input.val() is '' - $('.js-search-clear').addClass 'hidden' - else - $('.js-search-clear').removeClass 'hidden' - - clearSearchField: -> - $('.js-search-input') - .val '' - .trigger 'keyup' - .focus() diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js new file mode 100644 index 00000000000..990f6536eb2 --- /dev/null +++ b/app/assets/javascripts/search_autocomplete.js @@ -0,0 +1,360 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.SearchAutocomplete = (function() { + var KEYCODE; + + KEYCODE = { + ESCAPE: 27, + BACKSPACE: 8, + ENTER: 13 + }; + + function SearchAutocomplete(opts) { + var ref, ref1, ref2, ref3, ref4; + if (opts == null) { + opts = {}; + } + this.onSearchInputBlur = bind(this.onSearchInputBlur, this); + this.onClearInputClick = bind(this.onClearInputClick, this); + this.onSearchInputFocus = bind(this.onSearchInputFocus, this); + this.onSearchInputClick = bind(this.onSearchInputClick, this); + this.onSearchInputKeyUp = bind(this.onSearchInputKeyUp, this); + this.onSearchInputKeyDown = bind(this.onSearchInputKeyDown, this); + this.wrap = (ref = opts.wrap) != null ? ref : $('.search'), this.optsEl = (ref1 = opts.optsEl) != null ? ref1 : this.wrap.find('.search-autocomplete-opts'), this.autocompletePath = (ref2 = opts.autocompletePath) != null ? ref2 : this.optsEl.data('autocomplete-path'), this.projectId = (ref3 = opts.projectId) != null ? ref3 : this.optsEl.data('autocomplete-project-id') || '', this.projectRef = (ref4 = opts.projectRef) != null ? ref4 : this.optsEl.data('autocomplete-project-ref') || ''; + this.dropdown = this.wrap.find('.dropdown'); + this.dropdownContent = this.dropdown.find('.dropdown-content'); + this.locationBadgeEl = this.getElement('.location-badge'); + this.scopeInputEl = this.getElement('#scope'); + this.searchInput = this.getElement('.search-input'); + this.projectInputEl = this.getElement('#search_project_id'); + this.groupInputEl = this.getElement('#group_id'); + this.searchCodeInputEl = this.getElement('#search_code'); + this.repositoryInputEl = this.getElement('#repository_ref'); + this.clearInput = this.getElement('.js-clear-input'); + this.saveOriginalState(); + if (gon.current_user_id) { + this.createAutocomplete(); + } + this.searchInput.addClass('disabled'); + this.saveTextLength(); + this.bindEvents(); + } + + SearchAutocomplete.prototype.getElement = function(selector) { + return this.wrap.find(selector); + }; + + SearchAutocomplete.prototype.saveOriginalState = function() { + return this.originalState = this.serializeState(); + }; + + SearchAutocomplete.prototype.saveTextLength = function() { + return this.lastTextLength = this.searchInput.val().length; + }; + + SearchAutocomplete.prototype.createAutocomplete = function() { + return this.searchInput.glDropdown({ + filterInputBlur: false, + filterable: true, + filterRemote: true, + highlight: true, + enterCallback: false, + filterInput: 'input#search', + search: { + fields: ['text'] + }, + data: this.getData.bind(this), + selectable: true, + clicked: this.onClick.bind(this) + }); + }; + + SearchAutocomplete.prototype.getData = function(term, callback) { + var _this, contents, jqXHR; + _this = this; + if (!term) { + if (contents = this.getCategoryContents()) { + this.searchInput.data('glDropdown').filter.options.callback(contents); + this.enableAutocomplete(); + } + return; + } + if (this.loadingSuggestions) { + return; + } + this.loadingSuggestions = true; + return jqXHR = $.get(this.autocompletePath, { + project_id: this.projectId, + project_ref: this.projectRef, + term: term + }, function(response) { + var data, firstCategory, i, lastCategory, len, suggestion; + if (!response.length) { + _this.disableAutocomplete(); + return; + } + data = []; + firstCategory = true; + for (i = 0, len = response.length; i < len; i++) { + suggestion = response[i]; + if (lastCategory !== suggestion.category) { + if (!firstCategory) { + data.push('separator'); + } + if (firstCategory) { + firstCategory = false; + } + data.push({ + header: suggestion.category + }); + lastCategory = suggestion.category; + } + data.push({ + id: (suggestion.category.toLowerCase()) + "-" + suggestion.id, + category: suggestion.category, + text: suggestion.label, + url: suggestion.url + }); + } + if (data.length) { + data.push('separator'); + data.push({ + text: "Result name contains \"" + term + "\"", + url: "/search?search=" + term + "&project_id=" + (_this.projectInputEl.val()) + "&group_id=" + (_this.groupInputEl.val()) + }); + } + return callback(data); + }).always(function() { + return _this.loadingSuggestions = false; + }); + }; + + SearchAutocomplete.prototype.getCategoryContents = function() { + var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils; + userId = gon.current_user_id; + utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; + if (utils.isInGroupsPage() && groupOptions) { + options = groupOptions[utils.getGroupSlug()]; + } else if (utils.isInProjectPage() && projectOptions) { + options = projectOptions[utils.getProjectSlug()]; + } else if (dashboardOptions) { + options = dashboardOptions; + } + issuesPath = options.issuesPath, mrPath = options.mrPath, name = options.name; + items = [ + { + header: "" + name + }, { + text: 'Issues assigned to me', + url: issuesPath + "/?assignee_id=" + userId + }, { + text: "Issues I've created", + url: issuesPath + "/?author_id=" + userId + }, 'separator', { + text: 'Merge requests assigned to me', + url: mrPath + "/?assignee_id=" + userId + }, { + text: "Merge requests I've created", + url: mrPath + "/?author_id=" + userId + } + ]; + if (!name) { + items.splice(0, 1); + } + return items; + }; + + SearchAutocomplete.prototype.serializeState = function() { + return { + search_project_id: this.projectInputEl.val(), + group_id: this.groupInputEl.val(), + search_code: this.searchCodeInputEl.val(), + repository_ref: this.repositoryInputEl.val(), + scope: this.scopeInputEl.val(), + _location: this.locationBadgeEl.text() + }; + }; + + SearchAutocomplete.prototype.bindEvents = function() { + this.searchInput.on('keydown', this.onSearchInputKeyDown); + this.searchInput.on('keyup', this.onSearchInputKeyUp); + this.searchInput.on('click', this.onSearchInputClick); + this.searchInput.on('focus', this.onSearchInputFocus); + this.searchInput.on('blur', this.onSearchInputBlur); + this.clearInput.on('click', this.onClearInputClick); + return this.locationBadgeEl.on('click', (function(_this) { + return function() { + return _this.searchInput.focus(); + }; + })(this)); + }; + + SearchAutocomplete.prototype.enableAutocomplete = function() { + var _this; + if (!gon.current_user_id) { + return; + } + if (!this.dropdown.hasClass('open')) { + _this = this; + this.loadingSuggestions = false; + this.dropdown.addClass('open').trigger('shown.bs.dropdown'); + return this.searchInput.removeClass('disabled'); + } + }; + + SearchAutocomplete.prototype.onSearchInputKeyDown = function() { + return this.saveTextLength(); + }; + + SearchAutocomplete.prototype.onSearchInputKeyUp = function(e) { + switch (e.keyCode) { + case KEYCODE.BACKSPACE: + if (this.lastTextLength === 0 && this.badgePresent()) { + this.removeLocationBadge(); + } + if (this.lastTextLength === 1) { + this.disableAutocomplete(); + } + if (this.lastTextLength > 1) { + this.enableAutocomplete(); + } + break; + case KEYCODE.ESCAPE: + this.restoreOriginalState(); + break; + default: + if (this.searchInput.val() === '') { + this.disableAutocomplete(); + } else { + if (e.keyCode !== KEYCODE.ENTER) { + this.enableAutocomplete(); + } + } + } + this.wrap.toggleClass('has-value', !!e.target.value); + }; + + SearchAutocomplete.prototype.onSearchInputClick = function(e) { + return e.stopImmediatePropagation(); + }; + + SearchAutocomplete.prototype.onSearchInputFocus = function() { + this.isFocused = true; + this.wrap.addClass('search-active'); + if (this.getValue() === '') { + return this.getData(); + } + }; + + SearchAutocomplete.prototype.getValue = function() { + return this.searchInput.val(); + }; + + SearchAutocomplete.prototype.onClearInputClick = function(e) { + e.preventDefault(); + return this.searchInput.val('').focus(); + }; + + SearchAutocomplete.prototype.onSearchInputBlur = function(e) { + this.isFocused = false; + this.wrap.removeClass('search-active'); + if (this.searchInput.val() === '') { + return this.restoreOriginalState(); + } + }; + + SearchAutocomplete.prototype.addLocationBadge = function(item) { + var badgeText, category, value; + category = item.category != null ? item.category + ": " : ''; + value = item.value != null ? item.value : ''; + badgeText = "" + category + value; + this.locationBadgeEl.text(badgeText).show(); + return this.wrap.addClass('has-location-badge'); + }; + + SearchAutocomplete.prototype.hasLocationBadge = function() { + return this.wrap.is('.has-location-badge'); + }; + + SearchAutocomplete.prototype.restoreOriginalState = function() { + var i, input, inputs, len; + inputs = Object.keys(this.originalState); + for (i = 0, len = inputs.length; i < len; i++) { + input = inputs[i]; + this.getElement("#" + input).val(this.originalState[input]); + } + if (this.originalState._location === '') { + return this.locationBadgeEl.hide(); + } else { + return this.addLocationBadge({ + value: this.originalState._location + }); + } + }; + + SearchAutocomplete.prototype.badgePresent = function() { + return this.locationBadgeEl.length; + }; + + SearchAutocomplete.prototype.resetSearchState = function() { + var i, input, inputs, len, results; + inputs = Object.keys(this.originalState); + results = []; + for (i = 0, len = inputs.length; i < len; i++) { + input = inputs[i]; + if (input === '_location') { + break; + } + results.push(this.getElement("#" + input).val('')); + } + return results; + }; + + SearchAutocomplete.prototype.removeLocationBadge = function() { + this.locationBadgeEl.hide(); + this.resetSearchState(); + this.wrap.removeClass('has-location-badge'); + return this.disableAutocomplete(); + }; + + SearchAutocomplete.prototype.disableAutocomplete = function() { + this.searchInput.addClass('disabled'); + this.dropdown.removeClass('open'); + return this.restoreMenu(); + }; + + SearchAutocomplete.prototype.restoreMenu = function() { + var html; + html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>"; + return this.dropdownContent.html(html); + }; + + SearchAutocomplete.prototype.onClick = function(item, $el, e) { + if (location.pathname.indexOf(item.url) !== -1) { + e.preventDefault(); + if (!this.badgePresent) { + if (item.category === 'Projects') { + this.projectInputEl.val(item.id); + this.addLocationBadge({ + value: 'This project' + }); + } + if (item.category === 'Groups') { + this.groupInputEl.val(item.id); + this.addLocationBadge({ + value: 'This group' + }); + } + } + $el.removeClass('is-active'); + this.disableAutocomplete(); + return this.searchInput.val('').focus(); + } + }; + + return SearchAutocomplete; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee deleted file mode 100644 index 72b1d3dfb1e..00000000000 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ /dev/null @@ -1,334 +0,0 @@ -class @SearchAutocomplete - - KEYCODE = - ESCAPE: 27 - BACKSPACE: 8 - ENTER: 13 - - constructor: (opts = {}) -> - { - @wrap = $('.search') - - @optsEl = @wrap.find('.search-autocomplete-opts') - @autocompletePath = @optsEl.data('autocomplete-path') - @projectId = @optsEl.data('autocomplete-project-id') || '' - @projectRef = @optsEl.data('autocomplete-project-ref') ||Â '' - - } = opts - - # Dropdown Element - @dropdown = @wrap.find('.dropdown') - @dropdownContent = @dropdown.find('.dropdown-content') - - @locationBadgeEl = @getElement('.location-badge') - @scopeInputEl = @getElement('#scope') - @searchInput = @getElement('.search-input') - @projectInputEl = @getElement('#search_project_id') - @groupInputEl = @getElement('#group_id') - @searchCodeInputEl = @getElement('#search_code') - @repositoryInputEl = @getElement('#repository_ref') - @clearInput = @getElement('.js-clear-input') - - @saveOriginalState() - - # Only when user is logged in - @createAutocomplete() if gon.current_user_id - - @searchInput.addClass('disabled') - - @saveTextLength() - - @bindEvents() - - # Finds an element inside wrapper element - getElement: (selector) -> - @wrap.find(selector) - - saveOriginalState: -> - @originalState = @serializeState() - - saveTextLength: -> - @lastTextLength = @searchInput.val().length - - createAutocomplete: -> - @searchInput.glDropdown - filterInputBlur: false - filterable: true - filterRemote: true - highlight: true - enterCallback: false - filterInput: 'input#search' - search: - fields: ['text'] - data: @getData.bind(@) - selectable: true - clicked: @onClick.bind(@) - - getData: (term, callback) -> - _this = @ - - unless term - if contents = @getCategoryContents() - @searchInput.data('glDropdown').filter.options.callback contents - @enableAutocomplete() - - return - - # Prevent multiple ajax calls - return if @loadingSuggestions - - @loadingSuggestions = true - - jqXHR = $.get(@autocompletePath, { - project_id: @projectId - project_ref: @projectRef - term: term - }, (response) -> - # Hide dropdown menu if no suggestions returns - if !response.length - _this.disableAutocomplete() - return - - data = [] - - # List results - firstCategory = true - for suggestion in response - - # Add group header before list each group - if lastCategory isnt suggestion.category - data.push 'separator' if !firstCategory - - firstCategory = false if firstCategory - - data.push - header: suggestion.category - - lastCategory = suggestion.category - - data.push - id: "#{suggestion.category.toLowerCase()}-#{suggestion.id}" - category: suggestion.category - text: suggestion.label - url: suggestion.url - - # Add option to proceed with the search - if data.length - data.push('separator') - data.push - text: "Result name contains \"#{term}\"" - url: "/search?\ - search=#{term}\ - &project_id=#{_this.projectInputEl.val()}\ - &group_id=#{_this.groupInputEl.val()}" - - callback(data) - ).always -> - _this.loadingSuggestions = false - - - getCategoryContents: -> - - userId = gon.current_user_id - { utils, projectOptions, groupOptions, dashboardOptions } = gl - - if utils.isInGroupsPage() and groupOptions - options = groupOptions[utils.getGroupSlug()] - - else if utils.isInProjectPage() and projectOptions - options = projectOptions[utils.getProjectSlug()] - - else if dashboardOptions - options = dashboardOptions - - { issuesPath, mrPath, name } = options - - items = [ - { header: "#{name}" } - { text: 'Issues assigned to me', url: "#{issuesPath}/?assignee_id=#{userId}" } - { text: "Issues I've created", url: "#{issuesPath}/?author_id=#{userId}" } - 'separator' - { text: 'Merge requests assigned to me', url: "#{mrPath}/?assignee_id=#{userId}" } - { text: "Merge requests I've created", url: "#{mrPath}/?author_id=#{userId}" } - ] - - items.splice 0, 1 unless name - - return items - - - serializeState: -> - { - # Search Criteria - search_project_id: @projectInputEl.val() - group_id: @groupInputEl.val() - search_code: @searchCodeInputEl.val() - repository_ref: @repositoryInputEl.val() - scope: @scopeInputEl.val() - - # Location badge - _location: @locationBadgeEl.text() - } - - bindEvents: -> - @searchInput.on 'keydown', @onSearchInputKeyDown - @searchInput.on 'keyup', @onSearchInputKeyUp - @searchInput.on 'click', @onSearchInputClick - @searchInput.on 'focus', @onSearchInputFocus - @searchInput.on 'blur', @onSearchInputBlur - @clearInput.on 'click', @onClearInputClick - @locationBadgeEl.on 'click', => - @searchInput.focus() - - enableAutocomplete: -> - # No need to enable anything if user is not logged in - return if !gon.current_user_id - - unless @dropdown.hasClass('open') - _this = @ - @loadingSuggestions = false - - @dropdown - .addClass('open') - .trigger('shown.bs.dropdown') - @searchInput.removeClass('disabled') - - onSearchInputKeyDown: => - # Saves last length of the entered text - @saveTextLength() - - onSearchInputKeyUp: (e) => - switch e.keyCode - when KEYCODE.BACKSPACE - # when trying to remove the location badge - if @lastTextLength is 0 and @badgePresent() - @removeLocationBadge() - - # When removing the last character and no badge is present - if @lastTextLength is 1 - @disableAutocomplete() - - # When removing any character from existin value - if @lastTextLength > 1 - @enableAutocomplete() - - when KEYCODE.ESCAPE - @restoreOriginalState() - - else - # Handle the case when deleting the input value other than backspace - # e.g. Pressing ctrl + backspace or ctrl + x - if @searchInput.val() is '' - @disableAutocomplete() - else - # We should display the menu only when input is not empty - @enableAutocomplete() if e.keyCode isnt KEYCODE.ENTER - - @wrap.toggleClass 'has-value', !!e.target.value - - # Avoid falsy value to be returned - return - - onSearchInputClick: (e) => - # Prevents closing the dropdown menu - e.stopImmediatePropagation() - - onSearchInputFocus: => - @isFocused = true - @wrap.addClass('search-active') - - @getData() if @getValue() is '' - - - getValue: -> return @searchInput.val() - - - onClearInputClick: (e) => - e.preventDefault() - @searchInput.val('').focus() - - onSearchInputBlur: (e) => - @isFocused = false - @wrap.removeClass('search-active') - - # If input is blank then restore state - if @searchInput.val() is '' - @restoreOriginalState() - - addLocationBadge: (item) -> - category = if item.category? then "#{item.category}: " else '' - value = if item.value? then item.value else '' - - badgeText = "#{category}#{value}" - @locationBadgeEl.text(badgeText).show() - @wrap.addClass('has-location-badge') - - - hasLocationBadge: -> return @wrap.is '.has-location-badge' - - - restoreOriginalState: -> - inputs = Object.keys @originalState - - for input in inputs - @getElement("##{input}").val(@originalState[input]) - - if @originalState._location is '' - @locationBadgeEl.hide() - else - @addLocationBadge( - value: @originalState._location - ) - - badgePresent: -> - @locationBadgeEl.length - - resetSearchState: -> - inputs = Object.keys @originalState - - for input in inputs - - # _location isnt a input - break if input is '_location' - - @getElement("##{input}").val('') - - - removeLocationBadge: -> - - @locationBadgeEl.hide() - @resetSearchState() - @wrap.removeClass('has-location-badge') - @disableAutocomplete() - - - disableAutocomplete: -> - @searchInput.addClass('disabled') - @dropdown.removeClass('open') - @restoreMenu() - - restoreMenu: -> - html = "<ul> - <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> - </ul>" - @dropdownContent.html(html) - - onClick: (item, $el, e) -> - if location.pathname.indexOf(item.url) isnt -1 - e.preventDefault() - if not @badgePresent - if item.category is 'Projects' - @projectInputEl.val(item.id) - @addLocationBadge( - value: 'This project' - ) - - if item.category is 'Groups' - @groupInputEl.val(item.id) - @addLocationBadge( - value: 'This group' - ) - - $el.removeClass('is-active') - @disableAutocomplete() - @searchInput.val('').focus() diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js new file mode 100644 index 00000000000..3b28332854a --- /dev/null +++ b/app/assets/javascripts/shortcuts.js @@ -0,0 +1,97 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Shortcuts = (function() { + function Shortcuts(skipResetBindings) { + this.onToggleHelp = bind(this.onToggleHelp, this); + this.enabledHelp = []; + if (!skipResetBindings) { + Mousetrap.reset(); + } + Mousetrap.bind('?', this.onToggleHelp); + Mousetrap.bind('s', Shortcuts.focusSearch); + Mousetrap.bind('f', (function(_this) { + return function(e) { + return _this.focusFilter(e); + }; + })(this)); + Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview); + if (typeof findFileURL !== "undefined" && findFileURL !== null) { + Mousetrap.bind('t', function() { + return Turbolinks.visit(findFileURL); + }); + } + } + + Shortcuts.prototype.onToggleHelp = function(e) { + e.preventDefault(); + return Shortcuts.toggleHelp(this.enabledHelp); + }; + + Shortcuts.prototype.toggleMarkdownPreview = function(e) { + return $(document).triggerHandler('markdown-preview:toggle', [e]); + }; + + Shortcuts.toggleHelp = function(location) { + var $modal; + $modal = $('#modal-shortcuts'); + if ($modal.length) { + $modal.modal('toggle'); + return; + } + return $.ajax({ + url: gon.shortcuts_path, + dataType: 'script', + success: function(e) { + var i, l, len, results; + if (location && location.length > 0) { + results = []; + for (i = 0, len = location.length; i < len; i++) { + l = location[i]; + results.push($(l).show()); + } + return results; + } else { + $('.hidden-shortcut').show(); + return $('.js-more-help-button').remove(); + } + } + }); + }; + + Shortcuts.prototype.focusFilter = function(e) { + if (this.filterInput == null) { + this.filterInput = $('input[type=search]', '.nav-controls'); + } + this.filterInput.focus(); + return e.preventDefault(); + }; + + Shortcuts.focusSearch = function(e) { + $('#search').focus(); + return e.preventDefault(); + }; + + return Shortcuts; + + })(); + + $(document).on('click.more_help', '.js-more-help-button', function(e) { + $(this).remove(); + $('.hidden-shortcut').show(); + return e.preventDefault(); + }); + + Mousetrap.stopCallback = (function() { + var defaultStopCallback; + defaultStopCallback = Mousetrap.stopCallback; + return function(e, element, combo) { + if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) { + return false; + } else { + return defaultStopCallback.apply(this, arguments); + } + }; + })(); + +}).call(this); diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee deleted file mode 100644 index 8c8689bacee..00000000000 --- a/app/assets/javascripts/shortcuts.js.coffee +++ /dev/null @@ -1,60 +0,0 @@ -class @Shortcuts - constructor: (skipResetBindings) -> - @enabledHelp = [] - Mousetrap.reset() if not skipResetBindings - Mousetrap.bind '?', @onToggleHelp - Mousetrap.bind 's', Shortcuts.focusSearch - Mousetrap.bind 'f', (e) => @focusFilter e - Mousetrap.bind ['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview - Mousetrap.bind('t', -> Turbolinks.visit(findFileURL)) if findFileURL? - - onToggleHelp: (e) => - e.preventDefault() - Shortcuts.toggleHelp(@enabledHelp) - - toggleMarkdownPreview: (e) -> - $(document).triggerHandler('markdown-preview:toggle', [e]) - - @toggleHelp: (location) -> - $modal = $('#modal-shortcuts') - - if $modal.length - $modal.modal('toggle') - return - - $.ajax( - url: gon.shortcuts_path, - dataType: 'script', - success: (e) -> - if location and location.length > 0 - $(l).show() for l in location - else - $('.hidden-shortcut').show() - $('.js-more-help-button').remove() - ) - - focusFilter: (e) -> - @filterInput ?= $('input[type=search]', '.nav-controls') - @filterInput.focus() - e.preventDefault() - - @focusSearch: (e) -> - $('#search').focus() - e.preventDefault() - - -$(document).on 'click.more_help', '.js-more-help-button', (e) -> - $(@).remove() - $('.hidden-shortcut').show() - e.preventDefault() - -Mousetrap.stopCallback = (-> - defaultStopCallback = Mousetrap.stopCallback - - return (e, element, combo) -> - # allowed shortcuts if textarea, input, contenteditable are focused - if ['ctrl+shift+p', 'command+shift+p'].indexOf(combo) != -1 - return false - else - return defaultStopCallback.apply(@, arguments) -)() diff --git a/app/assets/javascripts/shortcuts_blob.coffee b/app/assets/javascripts/shortcuts_blob.coffee deleted file mode 100644 index 6d21e5d1150..00000000000 --- a/app/assets/javascripts/shortcuts_blob.coffee +++ /dev/null @@ -1,10 +0,0 @@ -#= require shortcuts - -class @ShortcutsBlob extends Shortcuts - constructor: (skipResetBindings) -> - super skipResetBindings - Mousetrap.bind('y', ShortcutsBlob.copyToClipboard) - - @copyToClipboard: -> - clipboardButton = $('.btn-clipboard') - clipboardButton.click() if clipboardButton diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js new file mode 100644 index 00000000000..b931eab638f --- /dev/null +++ b/app/assets/javascripts/shortcuts_blob.js @@ -0,0 +1,28 @@ + +/*= require shortcuts */ + +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.ShortcutsBlob = (function(superClass) { + extend(ShortcutsBlob, superClass); + + function ShortcutsBlob(skipResetBindings) { + ShortcutsBlob.__super__.constructor.call(this, skipResetBindings); + Mousetrap.bind('y', ShortcutsBlob.copyToClipboard); + } + + ShortcutsBlob.copyToClipboard = function() { + var clipboardButton; + clipboardButton = $('.btn-clipboard'); + if (clipboardButton) { + return clipboardButton.click(); + } + }; + + return ShortcutsBlob; + + })(Shortcuts); + +}).call(this); diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js new file mode 100644 index 00000000000..f7492a2aa5c --- /dev/null +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js @@ -0,0 +1,39 @@ + +/*= require shortcuts */ + +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.ShortcutsDashboardNavigation = (function(superClass) { + extend(ShortcutsDashboardNavigation, superClass); + + function ShortcutsDashboardNavigation() { + ShortcutsDashboardNavigation.__super__.constructor.call(this); + Mousetrap.bind('g a', function() { + return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-activity'); + }); + Mousetrap.bind('g i', function() { + return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-issues'); + }); + Mousetrap.bind('g m', function() { + return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-merge_requests'); + }); + Mousetrap.bind('g p', function() { + return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-projects'); + }); + } + + ShortcutsDashboardNavigation.findAndFollowLink = function(selector) { + var link; + link = $(selector).attr('href'); + if (link) { + return window.location = link; + } + }; + + return ShortcutsDashboardNavigation; + + })(Shortcuts); + +}).call(this); diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee b/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee deleted file mode 100644 index cca2b8a1fcc..00000000000 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee +++ /dev/null @@ -1,14 +0,0 @@ -#= require shortcuts - -class @ShortcutsDashboardNavigation extends Shortcuts - constructor: -> - super() - Mousetrap.bind('g a', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-activity')) - Mousetrap.bind('g i', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-issues')) - Mousetrap.bind('g m', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-merge_requests')) - Mousetrap.bind('g p', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-projects')) - - @findAndFollowLink: (selector) -> - link = $(selector).attr('href') - if link - window.location = link diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js new file mode 100644 index 00000000000..6c78914d338 --- /dev/null +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -0,0 +1,35 @@ + +/*= require shortcuts_navigation */ + +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.ShortcutsFindFile = (function(superClass) { + extend(ShortcutsFindFile, superClass); + + function ShortcutsFindFile(projectFindFile) { + var _oldStopCallback; + this.projectFindFile = projectFindFile; + ShortcutsFindFile.__super__.constructor.call(this); + _oldStopCallback = Mousetrap.stopCallback; + Mousetrap.stopCallback = (function(_this) { + return function(event, element, combo) { + if (element === _this.projectFindFile.inputElement[0] && (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')) { + event.preventDefault(); + return false; + } + return _oldStopCallback(event, element, combo); + }; + })(this); + Mousetrap.bind('up', this.projectFindFile.selectRowUp); + Mousetrap.bind('down', this.projectFindFile.selectRowDown); + Mousetrap.bind('esc', this.projectFindFile.goToTree); + Mousetrap.bind('enter', this.projectFindFile.goToBlob); + } + + return ShortcutsFindFile; + + })(ShortcutsNavigation); + +}).call(this); diff --git a/app/assets/javascripts/shortcuts_find_file.js.coffee b/app/assets/javascripts/shortcuts_find_file.js.coffee deleted file mode 100644 index 311e80bae19..00000000000 --- a/app/assets/javascripts/shortcuts_find_file.js.coffee +++ /dev/null @@ -1,19 +0,0 @@ -#= require shortcuts_navigation - -class @ShortcutsFindFile extends ShortcutsNavigation - constructor: (@projectFindFile) -> - super() - _oldStopCallback = Mousetrap.stopCallback - # override to fire shortcuts action when focus in textbox - Mousetrap.stopCallback = (event, element, combo) => - if element == @projectFindFile.inputElement[0] and (combo == 'up' or combo == 'down' or combo == 'esc' or combo == 'enter') - # when press up/down key in textbox, cusor prevent to move to home/end - event.preventDefault() - return false - - return _oldStopCallback(event, element, combo) - - Mousetrap.bind('up', @projectFindFile.selectRowUp) - Mousetrap.bind('down', @projectFindFile.selectRowDown) - Mousetrap.bind('esc', @projectFindFile.goToTree) - Mousetrap.bind('enter', @projectFindFile.goToBlob) diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee deleted file mode 100644 index c93bcf3ceec..00000000000 --- a/app/assets/javascripts/shortcuts_issuable.coffee +++ /dev/null @@ -1,53 +0,0 @@ -#= require mousetrap -#= require shortcuts_navigation - -class @ShortcutsIssuable extends ShortcutsNavigation - constructor: (isMergeRequest) -> - super() - Mousetrap.bind('a', @openSidebarDropdown.bind(@, 'assignee')) - Mousetrap.bind('m', @openSidebarDropdown.bind(@, 'milestone')) - Mousetrap.bind('r', => - @replyWithSelectedText() - return false - ) - Mousetrap.bind('e', => - @editIssue() - return false - ) - Mousetrap.bind('l', @openSidebarDropdown.bind(@, 'labels')) - - if isMergeRequest - @enabledHelp.push('.hidden-shortcut.merge_requests') - else - @enabledHelp.push('.hidden-shortcut.issues') - - replyWithSelectedText: -> - if window.getSelection - selected = window.getSelection().toString() - replyField = $('.js-main-target-form #note_note') - - return if selected.trim() == "" - - # Put a '>' character before each non-empty line in the selection - quote = _.map selected.split("\n"), (val) -> - "> #{val}\n" if val.trim() != '' - - # If replyField already has some content, add a newline before our quote - separator = replyField.val().trim() != "" and "\n" or '' - - replyField.val (_, current) -> - current + separator + quote.join('') + "\n" - - # Trigger autosave for the added text - replyField.trigger('input') - - # Focus the input field - replyField.focus() - - editIssue: -> - $editBtn = $('.issuable-edit') - Turbolinks.visit($editBtn.attr('href')) - - openSidebarDropdown: (name) -> - sidebar.openDropdown(name) - return false diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js new file mode 100644 index 00000000000..3f3a8a9dfd9 --- /dev/null +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -0,0 +1,75 @@ + +/*= require mousetrap */ + + +/*= require shortcuts_navigation */ + +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.ShortcutsIssuable = (function(superClass) { + extend(ShortcutsIssuable, superClass); + + function ShortcutsIssuable(isMergeRequest) { + ShortcutsIssuable.__super__.constructor.call(this); + Mousetrap.bind('a', this.openSidebarDropdown.bind(this, 'assignee')); + Mousetrap.bind('m', this.openSidebarDropdown.bind(this, 'milestone')); + Mousetrap.bind('r', (function(_this) { + return function() { + _this.replyWithSelectedText(); + return false; + }; + })(this)); + Mousetrap.bind('e', (function(_this) { + return function() { + _this.editIssue(); + return false; + }; + })(this)); + Mousetrap.bind('l', this.openSidebarDropdown.bind(this, 'labels')); + if (isMergeRequest) { + this.enabledHelp.push('.hidden-shortcut.merge_requests'); + } else { + this.enabledHelp.push('.hidden-shortcut.issues'); + } + } + + ShortcutsIssuable.prototype.replyWithSelectedText = function() { + var quote, replyField, selected, separator; + if (window.getSelection) { + selected = window.getSelection().toString(); + replyField = $('.js-main-target-form #note_note'); + if (selected.trim() === "") { + return; + } + quote = _.map(selected.split("\n"), function(val) { + if (val.trim() !== '') { + return "> " + val + "\n"; + } + }); + separator = replyField.val().trim() !== "" && "\n" || ''; + replyField.val(function(_, current) { + return current + separator + quote.join('') + "\n"; + }); + replyField.trigger('input'); + return replyField.focus(); + } + }; + + ShortcutsIssuable.prototype.editIssue = function() { + var $editBtn; + $editBtn = $('.issuable-edit'); + return Turbolinks.visit($editBtn.attr('href')); + }; + + ShortcutsIssuable.prototype.openSidebarDropdown = function(name) { + sidebar.openDropdown(name); + return false; + }; + + return ShortcutsIssuable; + + })(ShortcutsNavigation); + +}).call(this); diff --git a/app/assets/javascripts/shortcuts_navigation.coffee b/app/assets/javascripts/shortcuts_navigation.coffee deleted file mode 100644 index f39504e0645..00000000000 --- a/app/assets/javascripts/shortcuts_navigation.coffee +++ /dev/null @@ -1,23 +0,0 @@ -#= require shortcuts - -class @ShortcutsNavigation extends Shortcuts - constructor: -> - super() - Mousetrap.bind('g p', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-project')) - Mousetrap.bind('g e', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-project-activity')) - Mousetrap.bind('g f', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-tree')) - Mousetrap.bind('g c', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-commits')) - Mousetrap.bind('g b', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-builds')) - Mousetrap.bind('g n', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-network')) - Mousetrap.bind('g g', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-graphs')) - Mousetrap.bind('g i', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-issues')) - Mousetrap.bind('g m', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests')) - Mousetrap.bind('g w', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-wiki')) - Mousetrap.bind('g s', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-snippets')) - Mousetrap.bind('i', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-new-issue')) - @enabledHelp.push('.hidden-shortcut.project') - - @findAndFollowLink: (selector) -> - link = $(selector).attr('href') - if link - window.location = link diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js new file mode 100644 index 00000000000..469e25482bb --- /dev/null +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -0,0 +1,64 @@ + +/*= require shortcuts */ + +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.ShortcutsNavigation = (function(superClass) { + extend(ShortcutsNavigation, superClass); + + function ShortcutsNavigation() { + ShortcutsNavigation.__super__.constructor.call(this); + Mousetrap.bind('g p', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-project'); + }); + Mousetrap.bind('g e', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-project-activity'); + }); + Mousetrap.bind('g f', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-tree'); + }); + Mousetrap.bind('g c', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-commits'); + }); + Mousetrap.bind('g b', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-builds'); + }); + Mousetrap.bind('g n', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-network'); + }); + Mousetrap.bind('g g', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-graphs'); + }); + Mousetrap.bind('g i', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-issues'); + }); + Mousetrap.bind('g m', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests'); + }); + Mousetrap.bind('g w', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-wiki'); + }); + Mousetrap.bind('g s', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-snippets'); + }); + Mousetrap.bind('i', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-new-issue'); + }); + this.enabledHelp.push('.hidden-shortcut.project'); + } + + ShortcutsNavigation.findAndFollowLink = function(selector) { + var link; + link = $(selector).attr('href'); + if (link) { + return window.location = link; + } + }; + + return ShortcutsNavigation; + + })(Shortcuts); + +}).call(this); diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js new file mode 100644 index 00000000000..fb2b39e757e --- /dev/null +++ b/app/assets/javascripts/shortcuts_network.js @@ -0,0 +1,27 @@ + +/*= require shortcuts_navigation */ + +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + this.ShortcutsNetwork = (function(superClass) { + extend(ShortcutsNetwork, superClass); + + function ShortcutsNetwork(graph) { + this.graph = graph; + ShortcutsNetwork.__super__.constructor.call(this); + Mousetrap.bind(['left', 'h'], this.graph.scrollLeft); + Mousetrap.bind(['right', 'l'], this.graph.scrollRight); + Mousetrap.bind(['up', 'k'], this.graph.scrollUp); + Mousetrap.bind(['down', 'j'], this.graph.scrollDown); + Mousetrap.bind(['shift+up', 'shift+k'], this.graph.scrollTop); + Mousetrap.bind(['shift+down', 'shift+j'], this.graph.scrollBottom); + this.enabledHelp.push('.hidden-shortcut.network'); + } + + return ShortcutsNetwork; + + })(ShortcutsNavigation); + +}).call(this); diff --git a/app/assets/javascripts/shortcuts_network.js.coffee b/app/assets/javascripts/shortcuts_network.js.coffee deleted file mode 100644 index cc95ad7ebfe..00000000000 --- a/app/assets/javascripts/shortcuts_network.js.coffee +++ /dev/null @@ -1,12 +0,0 @@ -#= require shortcuts_navigation - -class @ShortcutsNetwork extends ShortcutsNavigation - constructor: (@graph) -> - super() - Mousetrap.bind(['left', 'h'], @graph.scrollLeft) - Mousetrap.bind(['right', 'l'], @graph.scrollRight) - Mousetrap.bind(['up', 'k'], @graph.scrollUp) - Mousetrap.bind(['down', 'j'], @graph.scrollDown) - Mousetrap.bind(['shift+up', 'shift+k'], @graph.scrollTop) - Mousetrap.bind(['shift+down', 'shift+j'], @graph.scrollBottom) - @enabledHelp.push('.hidden-shortcut.network') diff --git a/app/assets/javascripts/sidebar.js b/app/assets/javascripts/sidebar.js new file mode 100644 index 00000000000..bd0c1194b36 --- /dev/null +++ b/app/assets/javascripts/sidebar.js @@ -0,0 +1,41 @@ +(function() { + var collapsed, expanded, toggleSidebar; + + collapsed = 'page-sidebar-collapsed'; + + expanded = 'page-sidebar-expanded'; + + toggleSidebar = function() { + $('.page-with-sidebar').toggleClass(collapsed + " " + expanded); + $('.navbar-fixed-top').toggleClass("header-collapsed header-expanded"); + if ($.cookie('pin_nav') === 'true') { + $('.navbar-fixed-top').toggleClass('header-pinned-nav'); + $('.page-with-sidebar').toggleClass('page-sidebar-pinned'); + } + return setTimeout((function() { + var niceScrollBars; + niceScrollBars = $('.nav-sidebar').niceScroll(); + return niceScrollBars.updateScrollBar(); + }), 300); + }; + + $(document).off('click', 'body').on('click', 'body', function(e) { + var $nav, $target, $toggle, pageExpanded; + if ($.cookie('pin_nav') !== 'true') { + $target = $(e.target); + $nav = $target.closest('.sidebar-wrapper'); + pageExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded'); + $toggle = $target.closest('.toggle-nav-collapse, .side-nav-toggle'); + if ($nav.length === 0 && pageExpanded && $toggle.length === 0) { + $('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded'); + return $('.navbar-fixed-top').toggleClass('header-collapsed header-expanded'); + } + } + }); + + $(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', function(e) { + e.preventDefault(); + return toggleSidebar(); + }); + +}).call(this); diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee deleted file mode 100644 index 68009e58645..00000000000 --- a/app/assets/javascripts/sidebar.js.coffee +++ /dev/null @@ -1,37 +0,0 @@ -collapsed = 'page-sidebar-collapsed' -expanded = 'page-sidebar-expanded' - -toggleSidebar = -> - $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") - $('.navbar-fixed-top').toggleClass("header-collapsed header-expanded") - - if $.cookie('pin_nav') is 'true' - $('.navbar-fixed-top').toggleClass('header-pinned-nav') - $('.page-with-sidebar').toggleClass('page-sidebar-pinned') - - setTimeout ( -> - niceScrollBars = $('.nav-sidebar').niceScroll(); - niceScrollBars.updateScrollBar(); - ), 300 - -$(document) - .off 'click', 'body' - .on 'click', 'body', (e) -> - unless $.cookie('pin_nav') is 'true' - $target = $(e.target) - $nav = $target.closest('.sidebar-wrapper') - pageExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded') - $toggle = $target.closest('.toggle-nav-collapse, .side-nav-toggle') - - if $nav.length is 0 and pageExpanded and $toggle.length is 0 - $('.page-with-sidebar') - .toggleClass('page-sidebar-collapsed page-sidebar-expanded') - - $('.navbar-fixed-top') - .toggleClass('header-collapsed header-expanded') - -$(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', (e) -> - e.preventDefault() - - toggleSidebar() -) diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js new file mode 100644 index 00000000000..b9ae497b0e5 --- /dev/null +++ b/app/assets/javascripts/single_file_diff.js @@ -0,0 +1,77 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.SingleFileDiff = (function() { + var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER; + + WRAPPER = '<div class="diff-content diff-wrap-lines"></div>'; + + LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>'; + + ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>'; + + COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. Click to expand it.</div>'; + + function SingleFileDiff(file) { + this.file = file; + this.toggleDiff = bind(this.toggleDiff, this); + this.content = $('.diff-content', this.file); + this.diffForPath = this.content.find('[data-diff-for-path]').data('diff-for-path'); + this.isOpen = !this.diffForPath; + if (this.diffForPath) { + this.collapsedContent = this.content; + this.loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide(); + this.content = null; + this.collapsedContent.after(this.loadingContent); + } else { + this.collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide(); + this.content.after(this.collapsedContent); + } + this.collapsedContent.on('click', this.toggleDiff); + $('.file-title > a', this.file).on('click', this.toggleDiff); + } + + SingleFileDiff.prototype.toggleDiff = function(e) { + this.isOpen = !this.isOpen; + if (!this.isOpen && !this.hasError) { + this.content.hide(); + return this.collapsedContent.show(); + } else if (this.content) { + this.collapsedContent.hide(); + return this.content.show(); + } else { + return this.getContentHTML(); + } + }; + + SingleFileDiff.prototype.getContentHTML = function() { + this.collapsedContent.hide(); + this.loadingContent.show(); + $.get(this.diffForPath, (function(_this) { + return function(data) { + _this.loadingContent.hide(); + if (data.html) { + _this.content = $(data.html); + _this.content.syntaxHighlight(); + } else { + _this.hasError = true; + _this.content = $(ERROR_HTML); + } + return _this.collapsedContent.after(_this.content); + }; + })(this)); + }; + + return SingleFileDiff; + + })(); + + $.fn.singleFileDiff = function() { + return this.each(function() { + if (!$.data(this, 'singleFileDiff')) { + return $.data(this, 'singleFileDiff', new SingleFileDiff(this)); + } + }); + }; + +}).call(this); diff --git a/app/assets/javascripts/single_file_diff.js.coffee b/app/assets/javascripts/single_file_diff.js.coffee deleted file mode 100644 index f3e225c3728..00000000000 --- a/app/assets/javascripts/single_file_diff.js.coffee +++ /dev/null @@ -1,54 +0,0 @@ -class @SingleFileDiff - - WRAPPER = '<div class="diff-content diff-wrap-lines"></div>' - LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>' - ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>' - COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. Click to expand it.</div>' - - constructor: (@file) -> - @content = $('.diff-content', @file) - @diffForPath = @content.find('[data-diff-for-path]').data 'diff-for-path' - @isOpen = !@diffForPath - - if @diffForPath - @collapsedContent = @content - @loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide() - @content = null - @collapsedContent.after(@loadingContent) - else - @collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide() - @content.after(@collapsedContent) - - @collapsedContent.on 'click', @toggleDiff - - $('.file-title > a', @file).on 'click', @toggleDiff - - toggleDiff: (e) => - @isOpen = !@isOpen - if not @isOpen and not @hasError - @content.hide() - @collapsedContent.show() - else if @content - @collapsedContent.hide() - @content.show() - else - @getContentHTML() - - getContentHTML: -> - @collapsedContent.hide() - @loadingContent.show() - $.get @diffForPath, (data) => - @loadingContent.hide() - if data.html - @content = $(data.html) - @content.syntaxHighlight() - else - @hasError = true - @content = $(ERROR_HTML) - @collapsedContent.after(@content) - return - -$.fn.singleFileDiff = -> - return @each -> - if not $.data this, 'singleFileDiff' - $.data this, 'singleFileDiff', new SingleFileDiff this diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js new file mode 100644 index 00000000000..10509313c12 --- /dev/null +++ b/app/assets/javascripts/star.js @@ -0,0 +1,31 @@ +(function() { + this.Star = (function() { + function Star() { + $('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) { + var $starIcon, $starSpan, $this, toggleStar; + $this = $(this); + $starSpan = $this.find('span'); + $starIcon = $this.find('i'); + toggleStar = function(isStarred) { + $this.parent().find('.star-count').text(data.star_count); + if (isStarred) { + $starSpan.removeClass('starred').text('Star'); + gl.utils.updateTooltipTitle($this, 'Star project'); + $starIcon.removeClass('fa-star').addClass('fa-star-o'); + } else { + $starSpan.addClass('starred').text('Unstar'); + gl.utils.updateTooltipTitle($this, 'Unstar project'); + $starIcon.removeClass('fa-star-o').addClass('fa-star'); + } + }; + toggleStar($starSpan.hasClass('starred')); + }).on('ajax:error', function(e, xhr, status, error) { + new Flash('Star toggle failed. Try again later.', 'alert'); + }); + } + + return Star; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/star.js.coffee b/app/assets/javascripts/star.js.coffee deleted file mode 100644 index 01b28171f72..00000000000 --- a/app/assets/javascripts/star.js.coffee +++ /dev/null @@ -1,24 +0,0 @@ -class @Star - constructor: -> - $('.project-home-panel .toggle-star').on('ajax:success', (e, data, status, xhr) -> - $this = $(this) - $starSpan = $this.find('span') - $starIcon = $this.find('i') - - toggleStar = (isStarred) -> - $this.parent().find('.star-count').text data.star_count - if isStarred - $starSpan.removeClass('starred').text 'Star' - gl.utils.updateTooltipTitle $this, 'Star project' - $starIcon.removeClass('fa-star').addClass 'fa-star-o' - else - $starSpan.addClass('starred').text 'Unstar' - gl.utils.updateTooltipTitle $this, 'Unstar project' - $starIcon.removeClass('fa-star-o').addClass 'fa-star' - return - - toggleStar $starSpan.hasClass('starred') - return - ).on 'ajax:error', (e, xhr, status, error) -> - new Flash('Star toggle failed. Try again later.', 'alert') - return diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js new file mode 100644 index 00000000000..5e3c5983d75 --- /dev/null +++ b/app/assets/javascripts/subscription.js @@ -0,0 +1,41 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Subscription = (function() { + function Subscription(container) { + this.toggleSubscription = bind(this.toggleSubscription, this); + var $container; + $container = $(container); + this.url = $container.attr('data-url'); + this.subscribe_button = $container.find('.js-subscribe-button'); + this.subscription_status = $container.find('.subscription-status'); + this.subscribe_button.unbind('click').click(this.toggleSubscription); + } + + Subscription.prototype.toggleSubscription = function(event) { + var action, btn, current_status; + btn = $(event.currentTarget); + action = btn.find('span').text(); + current_status = this.subscription_status.attr('data-status'); + btn.addClass('disabled'); + return $.post(this.url, (function(_this) { + return function() { + var status; + btn.removeClass('disabled'); + status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed'; + _this.subscription_status.attr('data-status', status); + action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe'; + btn.find('span').text(action); + _this.subscription_status.find('>div').toggleClass('hidden'); + if (btn.attr('data-original-title')) { + return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle'); + } + }; + })(this)); + }; + + return Subscription; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee deleted file mode 100644 index 08d494aba9f..00000000000 --- a/app/assets/javascripts/subscription.js.coffee +++ /dev/null @@ -1,26 +0,0 @@ -class @Subscription - constructor: (container) -> - $container = $(container) - @url = $container.attr('data-url') - @subscribe_button = $container.find('.js-subscribe-button') - @subscription_status = $container.find('.subscription-status') - @subscribe_button.unbind('click').click(@toggleSubscription) - - toggleSubscription: (event) => - btn = $(event.currentTarget) - action = btn.find('span').text() - current_status = @subscription_status.attr('data-status') - btn.addClass('disabled') - - $.post @url, => - btn.removeClass('disabled') - status = if current_status == 'subscribed' then 'unsubscribed' else 'subscribed' - @subscription_status.attr('data-status', status) - action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe' - btn.find('span').text(action) - @subscription_status.find('>div').toggleClass('hidden') - - if btn.attr('data-original-title') - btn.tooltip('hide') - .attr('data-original-title', action) - .tooltip('fixTitle') diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js new file mode 100644 index 00000000000..d6c219603d1 --- /dev/null +++ b/app/assets/javascripts/subscription_select.js @@ -0,0 +1,35 @@ +(function() { + this.SubscriptionSelect = (function() { + function SubscriptionSelect() { + $('.js-subscription-event').each(function(i, el) { + var fieldName; + fieldName = $(el).data("field-name"); + return $(el).glDropdown({ + selectable: true, + fieldName: fieldName, + toggleLabel: (function(_this) { + return function(selected, el, instance) { + var $item, label; + label = 'Subscription'; + $item = instance.dropdown.find('.is-active'); + if ($item.length) { + label = $item.text(); + } + return label; + }; + })(this), + clicked: function(item, $el, e) { + return e.preventDefault(); + }, + id: function(obj, el) { + return $(el).data("id"); + } + }); + }); + } + + return SubscriptionSelect; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/subscription_select.js.coffee b/app/assets/javascripts/subscription_select.js.coffee deleted file mode 100644 index e5eb7a50d80..00000000000 --- a/app/assets/javascripts/subscription_select.js.coffee +++ /dev/null @@ -1,18 +0,0 @@ -class @SubscriptionSelect - constructor: -> - $('.js-subscription-event').each (i, el) -> - fieldName = $(el).data("field-name") - - $(el).glDropdown( - selectable: true - fieldName: fieldName - toggleLabel: (selected, el, instance) => - label = 'Subscription' - $item = instance.dropdown.find('.is-active') - label = $item.text() if $item.length - label - clicked: (item, $el, e)-> - e.preventDefault() - id: (obj, el) -> - $(el).data("id") - ) diff --git a/app/assets/javascripts/syntax_highlight.coffee b/app/assets/javascripts/syntax_highlight.coffee deleted file mode 100644 index 980f0232d10..00000000000 --- a/app/assets/javascripts/syntax_highlight.coffee +++ /dev/null @@ -1,20 +0,0 @@ -# Syntax Highlighter -# -# Applies a syntax highlighting color scheme CSS class to any element with the -# `js-syntax-highlight` class -# -# ### Example Markup -# -# <div class="js-syntax-highlight"></div> -# -$.fn.syntaxHighlight = -> - if $(this).hasClass('js-syntax-highlight') - # Given the element itself, apply highlighting - $(this).addClass(gon.user_color_scheme) - else - # Given a parent element, recurse to any of its applicable children - $children = $(this).find('.js-syntax-highlight') - $children.syntaxHighlight() if $children.length - -$(document).on 'ready page:load', -> - $('.js-syntax-highlight').syntaxHighlight() diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js new file mode 100644 index 00000000000..dba62638c78 --- /dev/null +++ b/app/assets/javascripts/syntax_highlight.js @@ -0,0 +1,18 @@ +(function() { + $.fn.syntaxHighlight = function() { + var $children; + if ($(this).hasClass('js-syntax-highlight')) { + return $(this).addClass(gon.user_color_scheme); + } else { + $children = $(this).find('.js-syntax-highlight'); + if ($children.length) { + return $children.syntaxHighlight(); + } + } + }; + + $(document).on('ready page:load', function() { + return $('.js-syntax-highlight').syntaxHighlight(); + }); + +}).call(this); diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js new file mode 100644 index 00000000000..6e677fa8cc6 --- /dev/null +++ b/app/assets/javascripts/todos.js @@ -0,0 +1,144 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Todos = (function() { + function Todos(opts) { + var ref; + if (opts == null) { + opts = {}; + } + this.allDoneClicked = bind(this.allDoneClicked, this); + this.doneClicked = bind(this.doneClicked, this); + this.el = (ref = opts.el) != null ? ref : $('.js-todos-options'); + this.perPage = this.el.data('perPage'); + this.clearListeners(); + this.initBtnListeners(); + } + + Todos.prototype.clearListeners = function() { + $('.done-todo').off('click'); + $('.js-todos-mark-all').off('click'); + return $('.todo').off('click'); + }; + + Todos.prototype.initBtnListeners = function() { + $('.done-todo').on('click', this.doneClicked); + $('.js-todos-mark-all').on('click', this.allDoneClicked); + return $('.todo').on('click', this.goToTodoUrl); + }; + + Todos.prototype.doneClicked = function(e) { + var $this; + e.preventDefault(); + e.stopImmediatePropagation(); + $this = $(e.currentTarget); + $this.disable(); + return $.ajax({ + type: 'POST', + url: $this.attr('href'), + dataType: 'json', + data: { + '_method': 'delete' + }, + success: (function(_this) { + return function(data) { + _this.redirectIfNeeded(data.count); + _this.clearDone($this.closest('li')); + return _this.updateBadges(data); + }; + })(this) + }); + }; + + Todos.prototype.allDoneClicked = function(e) { + var $this; + e.preventDefault(); + e.stopImmediatePropagation(); + $this = $(e.currentTarget); + $this.disable(); + return $.ajax({ + type: 'POST', + url: $this.attr('href'), + dataType: 'json', + data: { + '_method': 'delete' + }, + success: (function(_this) { + return function(data) { + $this.remove(); + $('.js-todos-list').remove(); + return _this.updateBadges(data); + }; + })(this) + }); + }; + + Todos.prototype.clearDone = function($row) { + var $ul; + $ul = $row.closest('ul'); + $row.remove(); + if (!$ul.find('li').length) { + return $ul.parents('.panel').remove(); + } + }; + + Todos.prototype.updateBadges = function(data) { + $('.todos-pending .badge, .todos-pending-count').text(data.count); + return $('.todos-done .badge').text(data.done_count); + }; + + Todos.prototype.getTotalPages = function() { + return this.el.data('totalPages'); + }; + + Todos.prototype.getCurrentPage = function() { + return this.el.data('currentPage'); + }; + + Todos.prototype.getTodosPerPage = function() { + return this.el.data('perPage'); + }; + + Todos.prototype.redirectIfNeeded = function(total) { + var currPage, currPages, newPages, pageParams, url; + currPages = this.getTotalPages(); + currPage = this.getCurrentPage(); + if (!total) { + location.reload(); + return; + } + if (!currPages) { + return; + } + newPages = Math.ceil(total / this.getTodosPerPage()); + url = location.href; + if (newPages !== currPages) { + if (currPages > 1 && currPage === currPages) { + pageParams = { + page: currPages - 1 + }; + url = gl.utils.mergeUrlParams(pageParams, url); + } + return Turbolinks.visit(url); + } + }; + + Todos.prototype.goToTodoUrl = function(e) { + var todoLink; + todoLink = $(this).data('url'); + if (!todoLink) { + return; + } + if (e.metaKey || e.which === 2) { + e.preventDefault(); + return window.open(todoLink, '_blank'); + } else { + return Turbolinks.visit(todoLink); + } + }; + + return Todos; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee deleted file mode 100644 index 10bef96f43d..00000000000 --- a/app/assets/javascripts/todos.js.coffee +++ /dev/null @@ -1,110 +0,0 @@ -class @Todos - constructor: (opts = {}) -> - { - @el = $('.js-todos-options') - } = opts - - @perPage = @el.data('perPage') - - @clearListeners() - @initBtnListeners() - - clearListeners: -> - $('.done-todo').off('click') - $('.js-todos-mark-all').off('click') - $('.todo').off('click') - - initBtnListeners: -> - $('.done-todo').on('click', @doneClicked) - $('.js-todos-mark-all').on('click', @allDoneClicked) - $('.todo').on('click', @goToTodoUrl) - - doneClicked: (e) => - e.preventDefault() - e.stopImmediatePropagation() - - $this = $(e.currentTarget) - $this.disable() - - $.ajax - type: 'POST' - url: $this.attr('href') - dataType: 'json' - data: '_method': 'delete' - success: (data) => - @redirectIfNeeded data.count - @clearDone $this.closest('li') - @updateBadges data - - allDoneClicked: (e) => - e.preventDefault() - e.stopImmediatePropagation() - - $this = $(e.currentTarget) - $this.disable() - - $.ajax - type: 'POST' - url: $this.attr('href') - dataType: 'json' - data: '_method': 'delete' - success: (data) => - $this.remove() - $('.js-todos-list').remove() - @updateBadges data - - clearDone: ($row) -> - $ul = $row.closest('ul') - $row.remove() - - if not $ul.find('li').length - $ul.parents('.panel').remove() - - updateBadges: (data) -> - $('.todos-pending .badge, .todos-pending-count').text data.count - $('.todos-done .badge').text data.done_count - - getTotalPages: -> - @el.data('totalPages') - - getCurrentPage: -> - @el.data('currentPage') - - getTodosPerPage: -> - @el.data('perPage') - - redirectIfNeeded: (total) -> - currPages = @getTotalPages() - currPage = @getCurrentPage() - - # Refresh if no remaining Todos - if not total - location.reload() - return - - # Do nothing if no pagination - return if not currPages - - newPages = Math.ceil(total / @getTodosPerPage()) - url = location.href # Includes query strings - - # If new total of pages is different than we have now - if newPages isnt currPages - # Redirect to previous page if there's one available - if currPages > 1 and currPage is currPages - pageParams = - page: currPages - 1 - url = gl.utils.mergeUrlParams(pageParams, url) - - Turbolinks.visit(url) - - goToTodoUrl: (e)-> - todoLink = $(this).data('url') - return unless todoLink - - # Allow Meta-Click or Mouse3-click to open in a new tab - if e.metaKey or e.which is 2 - e.preventDefault() - window.open(todoLink,'_blank') - else - Turbolinks.visit(todoLink) diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js new file mode 100644 index 00000000000..78e159a7ed9 --- /dev/null +++ b/app/assets/javascripts/tree.js @@ -0,0 +1,65 @@ +(function() { + this.TreeView = (function() { + function TreeView() { + this.initKeyNav(); + $(".tree-content-holder .tree-item").on('click', function(e) { + var $clickedEl, path; + $clickedEl = $(e.target); + path = $('.tree-item-file-name a', this).attr('href'); + if (!$clickedEl.is('a') && !$clickedEl.is('.str-truncated')) { + if (e.metaKey || e.which === 2) { + e.preventDefault(); + return window.open(path, '_blank'); + } else { + return Turbolinks.visit(path); + } + } + }); + $('span.log_loading:first').removeClass('hide'); + } + + TreeView.prototype.initKeyNav = function() { + var li, liSelected; + li = $("tr.tree-item"); + liSelected = null; + return $('body').keydown(function(e) { + var next, path; + if ($("input:focus").length > 0 && (e.which === 38 || e.which === 40)) { + return false; + } + if (e.which === 40) { + if (liSelected) { + next = liSelected.next(); + if (next.length > 0) { + liSelected.removeClass("selected"); + liSelected = next.addClass("selected"); + } + } else { + liSelected = li.eq(0).addClass("selected"); + } + return $(liSelected).focus(); + } else if (e.which === 38) { + if (liSelected) { + next = liSelected.prev(); + if (next.length > 0) { + liSelected.removeClass("selected"); + liSelected = next.addClass("selected"); + } + } else { + liSelected = li.last().addClass("selected"); + } + return $(liSelected).focus(); + } else if (e.which === 13) { + path = $('.tree-item.selected .tree-item-file-name a').attr('href'); + if (path) { + return Turbolinks.visit(path); + } + } + }); + }; + + return TreeView; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/tree.js.coffee b/app/assets/javascripts/tree.js.coffee deleted file mode 100644 index 83de584f2d9..00000000000 --- a/app/assets/javascripts/tree.js.coffee +++ /dev/null @@ -1,50 +0,0 @@ -class @TreeView - constructor: -> - @initKeyNav() - - # Code browser tree slider - # Make the entire tree-item row clickable, but not if clicking another link (like a commit message) - $(".tree-content-holder .tree-item").on 'click', (e) -> - $clickedEl = $(e.target) - path = $('.tree-item-file-name a', this).attr('href') - - if not $clickedEl.is('a') and not $clickedEl.is('.str-truncated') - if e.metaKey or e.which is 2 - e.preventDefault() - window.open path, '_blank' - else - Turbolinks.visit path - - # Show the "Loading commit data" for only the first element - $('span.log_loading:first').removeClass('hide') - - initKeyNav: -> - li = $("tr.tree-item") - liSelected = null - $('body').keydown (e) -> - if $("input:focus").length > 0 && (e.which == 38 || e.which == 40) - return false - - if e.which is 40 - if liSelected - next = liSelected.next() - if next.length > 0 - liSelected.removeClass "selected" - liSelected = next.addClass("selected") - else - liSelected = li.eq(0).addClass("selected") - - $(liSelected).focus() - else if e.which is 38 - if liSelected - next = liSelected.prev() - if next.length > 0 - liSelected.removeClass "selected" - liSelected = next.addClass("selected") - else - liSelected = li.last().addClass("selected") - - $(liSelected).focus() - else if e.which is 13 - path = $('.tree-item.selected .tree-item-file-name a').attr('href') - if path then Turbolinks.visit(path) diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js new file mode 100644 index 00000000000..9ba847fb0c2 --- /dev/null +++ b/app/assets/javascripts/u2f/authenticate.js @@ -0,0 +1,89 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.U2FAuthenticate = (function() { + function U2FAuthenticate(container, u2fParams) { + this.container = container; + this.renderNotSupported = bind(this.renderNotSupported, this); + this.renderAuthenticated = bind(this.renderAuthenticated, this); + this.renderError = bind(this.renderError, this); + this.renderInProgress = bind(this.renderInProgress, this); + this.renderSetup = bind(this.renderSetup, this); + this.renderTemplate = bind(this.renderTemplate, this); + this.authenticate = bind(this.authenticate, this); + this.start = bind(this.start, this); + this.appId = u2fParams.app_id; + this.challenge = u2fParams.challenge; + this.signRequests = u2fParams.sign_requests.map(function(request) { + return _(request).omit('challenge'); + }); + } + + U2FAuthenticate.prototype.start = function() { + if (U2FUtil.isU2FSupported()) { + return this.renderSetup(); + } else { + return this.renderNotSupported(); + } + }; + + U2FAuthenticate.prototype.authenticate = function() { + return u2f.sign(this.appId, this.challenge, this.signRequests, (function(_this) { + return function(response) { + var error; + if (response.errorCode) { + error = new U2FError(response.errorCode); + return _this.renderError(error); + } else { + return _this.renderAuthenticated(JSON.stringify(response)); + } + }; + })(this), 10); + }; + + U2FAuthenticate.prototype.templates = { + "notSupported": "#js-authenticate-u2f-not-supported", + "setup": '#js-authenticate-u2f-setup', + "inProgress": '#js-authenticate-u2f-in-progress', + "error": '#js-authenticate-u2f-error', + "authenticated": '#js-authenticate-u2f-authenticated' + }; + + U2FAuthenticate.prototype.renderTemplate = function(name, params) { + var template, templateString; + templateString = $(this.templates[name]).html(); + template = _.template(templateString); + return this.container.html(template(params)); + }; + + U2FAuthenticate.prototype.renderSetup = function() { + this.renderTemplate('setup'); + return this.container.find('#js-login-u2f-device').on('click', this.renderInProgress); + }; + + U2FAuthenticate.prototype.renderInProgress = function() { + this.renderTemplate('inProgress'); + return this.authenticate(); + }; + + U2FAuthenticate.prototype.renderError = function(error) { + this.renderTemplate('error', { + error_message: error.message() + }); + return this.container.find('#js-u2f-try-again').on('click', this.renderSetup); + }; + + U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) { + this.renderTemplate('authenticated'); + return this.container.find("#js-device-response").val(deviceResponse); + }; + + U2FAuthenticate.prototype.renderNotSupported = function() { + return this.renderTemplate('notSupported'); + }; + + return U2FAuthenticate; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/u2f/authenticate.js.coffee b/app/assets/javascripts/u2f/authenticate.js.coffee deleted file mode 100644 index 918c0a560fd..00000000000 --- a/app/assets/javascripts/u2f/authenticate.js.coffee +++ /dev/null @@ -1,75 +0,0 @@ -# Authenticate U2F (universal 2nd factor) devices for users to authenticate with. -# -# State Flow #1: setup -> in_progress -> authenticated -> POST to server -# State Flow #2: setup -> in_progress -> error -> setup - -class @U2FAuthenticate - constructor: (@container, u2fParams) -> - @appId = u2fParams.app_id - @challenge = u2fParams.challenge - - # The U2F Javascript API v1.1 requires a single challenge, with - # _no challenges per-request_. The U2F Javascript API v1.0 requires a - # challenge per-request, which is done by copying the single challenge - # into every request. - # - # In either case, we don't need the per-request challenges that the server - # has generated, so we can remove them. - # - # Note: The server library fixes this behaviour in (unreleased) version 1.0.0. - # This can be removed once we upgrade. - # https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4 - @signRequests = u2fParams.sign_requests.map (request) -> _(request).omit('challenge') - - start: () => - if U2FUtil.isU2FSupported() - @renderSetup() - else - @renderNotSupported() - - authenticate: () => - u2f.sign(@appId, @challenge, @signRequests, (response) => - if response.errorCode - error = new U2FError(response.errorCode) - @renderError(error); - else - @renderAuthenticated(JSON.stringify(response)) - , 10) - - ############# - # Rendering # - ############# - - templates: { - "notSupported": "#js-authenticate-u2f-not-supported", - "setup": '#js-authenticate-u2f-setup', - "inProgress": '#js-authenticate-u2f-in-progress', - "error": '#js-authenticate-u2f-error', - "authenticated": '#js-authenticate-u2f-authenticated' - } - - renderTemplate: (name, params) => - templateString = $(@templates[name]).html() - template = _.template(templateString) - @container.html(template(params)) - - renderSetup: () => - @renderTemplate('setup') - @container.find('#js-login-u2f-device').on('click', @renderInProgress) - - renderInProgress: () => - @renderTemplate('inProgress') - @authenticate() - - renderError: (error) => - @renderTemplate('error', {error_message: error.message()}) - @container.find('#js-u2f-try-again').on('click', @renderSetup) - - renderAuthenticated: (deviceResponse) => - @renderTemplate('authenticated') - # Prefer to do this instead of interpolating using Underscore templates - # because of JSON escaping issues. - @container.find("#js-device-response").val(deviceResponse) - - renderNotSupported: () => - @renderTemplate('notSupported') diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js new file mode 100644 index 00000000000..bc48c67c4f2 --- /dev/null +++ b/app/assets/javascripts/u2f/error.js @@ -0,0 +1,27 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.U2FError = (function() { + function U2FError(errorCode) { + this.errorCode = errorCode; + this.message = bind(this.message, this); + this.httpsDisabled = window.location.protocol !== 'https:'; + console.error("U2F Error Code: " + this.errorCode); + } + + U2FError.prototype.message = function() { + switch (false) { + case !(this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled): + return "U2F only works with HTTPS-enabled websites. Contact your administrator for more details."; + case this.errorCode !== u2f.ErrorCodes.DEVICE_INELIGIBLE: + return "This device has already been registered with us."; + default: + return "There was a problem communicating with your device."; + } + }; + + return U2FError; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/u2f/error.js.coffee b/app/assets/javascripts/u2f/error.js.coffee deleted file mode 100644 index 1a2fc3e757f..00000000000 --- a/app/assets/javascripts/u2f/error.js.coffee +++ /dev/null @@ -1,13 +0,0 @@ -class @U2FError - constructor: (@errorCode) -> - @httpsDisabled = (window.location.protocol isnt 'https:') - console.error("U2F Error Code: #{@errorCode}") - - message: () => - switch - when (@errorCode is u2f.ErrorCodes.BAD_REQUEST and @httpsDisabled) - "U2F only works with HTTPS-enabled websites. Contact your administrator for more details." - when @errorCode is u2f.ErrorCodes.DEVICE_INELIGIBLE - "This device has already been registered with us." - else - "There was a problem communicating with your device." diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js new file mode 100644 index 00000000000..c87e0840df3 --- /dev/null +++ b/app/assets/javascripts/u2f/register.js @@ -0,0 +1,87 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.U2FRegister = (function() { + function U2FRegister(container, u2fParams) { + this.container = container; + this.renderNotSupported = bind(this.renderNotSupported, this); + this.renderRegistered = bind(this.renderRegistered, this); + this.renderError = bind(this.renderError, this); + this.renderInProgress = bind(this.renderInProgress, this); + this.renderSetup = bind(this.renderSetup, this); + this.renderTemplate = bind(this.renderTemplate, this); + this.register = bind(this.register, this); + this.start = bind(this.start, this); + this.appId = u2fParams.app_id; + this.registerRequests = u2fParams.register_requests; + this.signRequests = u2fParams.sign_requests; + } + + U2FRegister.prototype.start = function() { + if (U2FUtil.isU2FSupported()) { + return this.renderSetup(); + } else { + return this.renderNotSupported(); + } + }; + + U2FRegister.prototype.register = function() { + return u2f.register(this.appId, this.registerRequests, this.signRequests, (function(_this) { + return function(response) { + var error; + if (response.errorCode) { + error = new U2FError(response.errorCode); + return _this.renderError(error); + } else { + return _this.renderRegistered(JSON.stringify(response)); + } + }; + })(this), 10); + }; + + U2FRegister.prototype.templates = { + "notSupported": "#js-register-u2f-not-supported", + "setup": '#js-register-u2f-setup', + "inProgress": '#js-register-u2f-in-progress', + "error": '#js-register-u2f-error', + "registered": '#js-register-u2f-registered' + }; + + U2FRegister.prototype.renderTemplate = function(name, params) { + var template, templateString; + templateString = $(this.templates[name]).html(); + template = _.template(templateString); + return this.container.html(template(params)); + }; + + U2FRegister.prototype.renderSetup = function() { + this.renderTemplate('setup'); + return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress); + }; + + U2FRegister.prototype.renderInProgress = function() { + this.renderTemplate('inProgress'); + return this.register(); + }; + + U2FRegister.prototype.renderError = function(error) { + this.renderTemplate('error', { + error_message: error.message() + }); + return this.container.find('#js-u2f-try-again').on('click', this.renderSetup); + }; + + U2FRegister.prototype.renderRegistered = function(deviceResponse) { + this.renderTemplate('registered'); + return this.container.find("#js-device-response").val(deviceResponse); + }; + + U2FRegister.prototype.renderNotSupported = function() { + return this.renderTemplate('notSupported'); + }; + + return U2FRegister; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/u2f/register.js.coffee b/app/assets/javascripts/u2f/register.js.coffee deleted file mode 100644 index 74472cfa120..00000000000 --- a/app/assets/javascripts/u2f/register.js.coffee +++ /dev/null @@ -1,63 +0,0 @@ -# Register U2F (universal 2nd factor) devices for users to authenticate with. -# -# State Flow #1: setup -> in_progress -> registered -> POST to server -# State Flow #2: setup -> in_progress -> error -> setup - -class @U2FRegister - constructor: (@container, u2fParams) -> - @appId = u2fParams.app_id - @registerRequests = u2fParams.register_requests - @signRequests = u2fParams.sign_requests - - start: () => - if U2FUtil.isU2FSupported() - @renderSetup() - else - @renderNotSupported() - - register: () => - u2f.register(@appId, @registerRequests, @signRequests, (response) => - if response.errorCode - error = new U2FError(response.errorCode) - @renderError(error); - else - @renderRegistered(JSON.stringify(response)) - , 10) - - ############# - # Rendering # - ############# - - templates: { - "notSupported": "#js-register-u2f-not-supported", - "setup": '#js-register-u2f-setup', - "inProgress": '#js-register-u2f-in-progress', - "error": '#js-register-u2f-error', - "registered": '#js-register-u2f-registered' - } - - renderTemplate: (name, params) => - templateString = $(@templates[name]).html() - template = _.template(templateString) - @container.html(template(params)) - - renderSetup: () => - @renderTemplate('setup') - @container.find('#js-setup-u2f-device').on('click', @renderInProgress) - - renderInProgress: () => - @renderTemplate('inProgress') - @register() - - renderError: (error) => - @renderTemplate('error', {error_message: error.message()}) - @container.find('#js-u2f-try-again').on('click', @renderSetup) - - renderRegistered: (deviceResponse) => - @renderTemplate('registered') - # Prefer to do this instead of interpolating using Underscore templates - # because of JSON escaping issues. - @container.find("#js-device-response").val(deviceResponse) - - renderNotSupported: () => - @renderTemplate('notSupported') diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js new file mode 100644 index 00000000000..907e640161a --- /dev/null +++ b/app/assets/javascripts/u2f/util.js @@ -0,0 +1,13 @@ +(function() { + this.U2FUtil = (function() { + function U2FUtil() {} + + U2FUtil.isU2FSupported = function() { + return window.u2f; + }; + + return U2FUtil; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/u2f/util.js.coffee b/app/assets/javascripts/u2f/util.js.coffee deleted file mode 100644 index 5ef324f609d..00000000000 --- a/app/assets/javascripts/u2f/util.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -class @U2FUtil - @isU2FSupported: -> - window.u2f diff --git a/app/assets/javascripts/user.js b/app/assets/javascripts/user.js new file mode 100644 index 00000000000..b46390ad8f4 --- /dev/null +++ b/app/assets/javascripts/user.js @@ -0,0 +1,31 @@ +(function() { + this.User = (function() { + function User(opts) { + this.opts = opts; + $('.profile-groups-avatars').tooltip({ + "placement": "top" + }); + this.initTabs(); + $('.hide-project-limit-message').on('click', function(e) { + var path; + path = '/'; + $.cookie('hide_project_limit_message', 'false', { + path: path + }); + $(this).parents('.project-limit-message').remove(); + return e.preventDefault(); + }); + } + + User.prototype.initTabs = function() { + return new UserTabs({ + parentEl: '.user-profile', + action: this.opts.action + }); + }; + + return User; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/user.js.coffee b/app/assets/javascripts/user.js.coffee deleted file mode 100644 index 2882a90d118..00000000000 --- a/app/assets/javascripts/user.js.coffee +++ /dev/null @@ -1,17 +0,0 @@ -class @User - constructor: (@opts) -> - $('.profile-groups-avatars').tooltip("placement": "top") - - @initTabs() - - $('.hide-project-limit-message').on 'click', (e) -> - path = '/' - $.cookie('hide_project_limit_message', 'false', { path: path }) - $(@).parents('.project-limit-message').remove() - e.preventDefault() - - initTabs: -> - new UserTabs( - parentEl: '.user-profile' - action: @opts.action - ) diff --git a/app/assets/javascripts/user_tabs.js b/app/assets/javascripts/user_tabs.js new file mode 100644 index 00000000000..e5e75701fee --- /dev/null +++ b/app/assets/javascripts/user_tabs.js @@ -0,0 +1,119 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.UserTabs = (function() { + function UserTabs(opts) { + this.tabShown = bind(this.tabShown, this); + var i, item, len, ref, ref1, ref2, ref3; + this.action = (ref = opts.action) != null ? ref : 'activity', this.defaultAction = (ref1 = opts.defaultAction) != null ? ref1 : 'activity', this.parentEl = (ref2 = opts.parentEl) != null ? ref2 : $(document); + if (typeof this.parentEl === 'string') { + this.parentEl = $(this.parentEl); + } + this._location = location; + this.loaded = {}; + ref3 = this.parentEl.find('.nav-links a'); + for (i = 0, len = ref3.length; i < len; i++) { + item = ref3[i]; + this.loaded[$(item).attr('data-action')] = false; + } + this.actions = Object.keys(this.loaded); + this.bindEvents(); + if (this.action === 'show') { + this.action = this.defaultAction; + } + this.activateTab(this.action); + } + + UserTabs.prototype.bindEvents = function() { + return this.parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]').on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', this.tabShown); + }; + + UserTabs.prototype.tabShown = function(event) { + var $target, action, source; + $target = $(event.target); + action = $target.data('action'); + source = $target.attr('href'); + this.setTab(source, action); + return this.setCurrentAction(action); + }; + + UserTabs.prototype.activateTab = function(action) { + return this.parentEl.find(".nav-links .js-" + action + "-tab a").tab('show'); + }; + + UserTabs.prototype.setTab = function(source, action) { + if (this.loaded[action] === true) { + return; + } + if (action === 'activity') { + this.loadActivities(source); + } + if (action === 'groups' || action === 'contributed' || action === 'projects' || action === 'snippets') { + return this.loadTab(source, action); + } + }; + + UserTabs.prototype.loadTab = function(source, action) { + return $.ajax({ + beforeSend: (function(_this) { + return function() { + return _this.toggleLoading(true); + }; + })(this), + complete: (function(_this) { + return function() { + return _this.toggleLoading(false); + }; + })(this), + dataType: 'json', + type: 'GET', + url: source + ".json", + success: (function(_this) { + return function(data) { + var tabSelector; + tabSelector = 'div#' + action; + _this.parentEl.find(tabSelector).html(data.html); + _this.loaded[action] = true; + return gl.utils.localTimeAgo($('.js-timeago', tabSelector)); + }; + })(this) + }); + }; + + UserTabs.prototype.loadActivities = function(source) { + var $calendarWrap; + if (this.loaded['activity'] === true) { + return; + } + $calendarWrap = this.parentEl.find('.user-calendar'); + $calendarWrap.load($calendarWrap.data('href')); + new Activities(); + return this.loaded['activity'] = true; + }; + + UserTabs.prototype.toggleLoading = function(status) { + return this.parentEl.find('.loading-status .loading').toggle(status); + }; + + UserTabs.prototype.setCurrentAction = function(action) { + var new_state, regExp; + regExp = new RegExp('\/(' + this.actions.join('|') + ')(\.html)?\/?$'); + new_state = this._location.pathname; + new_state = new_state.replace(/\/+$/, ""); + new_state = new_state.replace(regExp, ''); + if (action !== this.defaultAction) { + new_state += "/" + action; + } + new_state += this._location.search + this._location.hash; + history.replaceState({ + turbolinks: true, + url: new_state + }, document.title, new_state); + return new_state; + }; + + return UserTabs; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/user_tabs.js.coffee b/app/assets/javascripts/user_tabs.js.coffee deleted file mode 100644 index 29dad21faed..00000000000 --- a/app/assets/javascripts/user_tabs.js.coffee +++ /dev/null @@ -1,156 +0,0 @@ -# UserTabs -# -# Handles persisting and restoring the current tab selection and lazily-loading -# content on the Users#show page. -# -# ### Example Markup -# -# <ul class="nav-links"> -# <li class="activity-tab active"> -# <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username"> -# Activity -# </a> -# </li> -# <li class="groups-tab"> -# <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups"> -# Groups -# </a> -# </li> -# <li class="contributed-tab"> -# <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed"> -# Contributed projects -# </a> -# </li> -# <li class="projects-tab"> -# <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects"> -# Personal projects -# </a> -# </li> -# <li class="snippets-tab"> -# <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets"> -# </a> -# </li> -# </ul> -# -# <div class="tab-content"> -# <div class="tab-pane" id="activity"> -# Activity Content -# </div> -# <div class="tab-pane" id="groups"> -# Groups Content -# </div> -# <div class="tab-pane" id="contributed"> -# Contributed projects content -# </div> -# <div class="tab-pane" id="projects"> -# Projects content -# </div> -# <div class="tab-pane" id="snippets"> -# Snippets content -# </div> -# </div> -# -# <div class="loading-status"> -# <div class="loading"> -# Loading Animation -# </div> -# </div> -# -class @UserTabs - constructor: (opts) -> - { - @action = 'activity' - @defaultAction = 'activity' - @parentEl = $(document) - } = opts - - # Make jQuery object if selector is provided - @parentEl = $(@parentEl) if typeof @parentEl is 'string' - - # Store the `location` object, allowing for easier stubbing in tests - @_location = location - - # Set tab states - @loaded = {} - for item in @parentEl.find('.nav-links a') - @loaded[$(item).attr 'data-action'] = false - - # Actions - @actions = Object.keys @loaded - - @bindEvents() - - # Set active tab - @action = @defaultAction if @action is 'show' - @activateTab(@action) - - bindEvents: -> - # Toggle event listeners - @parentEl - .off 'shown.bs.tab', '.nav-links a[data-toggle="tab"]' - .on 'shown.bs.tab', '.nav-links a[data-toggle="tab"]', @tabShown - - tabShown: (event) => - $target = $(event.target) - action = $target.data('action') - source = $target.attr('href') - - @setTab(source, action) - @setCurrentAction(action) - - activateTab: (action) -> - @parentEl.find(".nav-links .js-#{action}-tab a").tab('show') - - setTab: (source, action) -> - return if @loaded[action] is true - - if action is 'activity' - @loadActivities(source) - - if action in ['groups', 'contributed', 'projects', 'snippets'] - @loadTab(source, action) - - loadTab: (source, action) -> - $.ajax - beforeSend: => @toggleLoading(true) - complete: => @toggleLoading(false) - dataType: 'json' - type: 'GET' - url: "#{source}.json" - success: (data) => - tabSelector = 'div#' + action - @parentEl.find(tabSelector).html(data.html) - @loaded[action] = true - - # Fix tooltips - gl.utils.localTimeAgo($('.js-timeago', tabSelector)) - - loadActivities: (source) -> - return if @loaded['activity'] is true - - $calendarWrap = @parentEl.find('.user-calendar') - $calendarWrap.load($calendarWrap.data('href')) - - new Activities() - @loaded['activity'] = true - - toggleLoading: (status) -> - @parentEl.find('.loading-status .loading').toggle(status) - - setCurrentAction: (action) -> - # Remove possible actions from URL - regExp = new RegExp('\/(' + @actions.join('|') + ')(\.html)?\/?$') - new_state = @_location.pathname - new_state = new_state.replace(/\/+$/, "") # remove trailing slashes - new_state = new_state.replace(regExp, '') - - # Append the new action if we're on a tab other than 'activity' - unless action == @defaultAction - new_state += "/#{action}" - - # Ensure parameters and hash come along for the ride - new_state += @_location.search + @_location.hash - - history.replaceState {turbolinks: true, url: new_state}, document.title, new_state - - new_state diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js new file mode 100644 index 00000000000..8b3dbf5f5ae --- /dev/null +++ b/app/assets/javascripts/users/calendar.js @@ -0,0 +1,192 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Calendar = (function() { + function Calendar(timestamps, calendar_activities_path) { + var group, i; + this.calendar_activities_path = calendar_activities_path; + this.clickDay = bind(this.clickDay, this); + this.currentSelectedDate = ''; + this.daySpace = 1; + this.daySize = 15; + this.daySizeWithSpace = this.daySize + (this.daySpace * 2); + this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + this.months = []; + this.timestampsTmp = []; + i = 0; + group = 0; + _.each(timestamps, (function(_this) { + return function(count, date) { + var day, innerArray, newDate; + newDate = new Date(parseInt(date) * 1000); + day = newDate.getDay(); + if ((day === 0 && i !== 0) || i === 0) { + _this.timestampsTmp.push([]); + group++; + } + innerArray = _this.timestampsTmp[group - 1]; + innerArray.push({ + count: count, + date: newDate, + day: day + }); + return i++; + }; + })(this)); + this.colorKey = this.initColorKey(); + this.color = this.initColor(); + this.renderSvg(group); + this.renderDays(); + this.renderMonths(); + this.renderDayTitles(); + this.renderKey(); + this.initTooltips(); + } + + Calendar.prototype.renderSvg = function(group) { + return this.svg = d3.select('.js-contrib-calendar').append('svg').attr('width', (group + 1) * this.daySizeWithSpace).attr('height', 167).attr('class', 'contrib-calendar'); + }; + + Calendar.prototype.renderDays = function() { + return this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g').attr('transform', (function(_this) { + return function(group, i) { + _.each(group, function(stamp, a) { + var lastMonth, lastMonthX, month, x; + if (a === 0 && stamp.day === 0) { + month = stamp.date.getMonth(); + x = (_this.daySizeWithSpace * i + 1) + _this.daySizeWithSpace; + lastMonth = _.last(_this.months); + if (lastMonth != null) { + lastMonthX = lastMonth.x; + } + if (lastMonth == null) { + return _this.months.push({ + month: month, + x: x + }); + } else if (month !== lastMonth.month && x - _this.daySizeWithSpace !== lastMonthX) { + return _this.months.push({ + month: month, + x: x + }); + } + } + }); + return "translate(" + ((_this.daySizeWithSpace * i + 1) + _this.daySizeWithSpace) + ", 18)"; + }; + })(this)).selectAll('rect').data(function(stamp) { + return stamp; + }).enter().append('rect').attr('x', '0').attr('y', (function(_this) { + return function(stamp, i) { + return _this.daySizeWithSpace * stamp.day; + }; + })(this)).attr('width', this.daySize).attr('height', this.daySize).attr('title', (function(_this) { + return function(stamp) { + var contribText, date, dateText; + date = new Date(stamp.date); + contribText = 'No contributions'; + if (stamp.count > 0) { + contribText = stamp.count + " contribution" + (stamp.count > 1 ? 's' : ''); + } + dateText = dateFormat(date, 'mmm d, yyyy'); + return contribText + "<br />" + (gl.utils.getDayName(date)) + " " + dateText; + }; + })(this)).attr('class', 'user-contrib-cell js-tooltip').attr('fill', (function(_this) { + return function(stamp) { + if (stamp.count !== 0) { + return _this.color(Math.min(stamp.count, 40)); + } else { + return '#ededed'; + } + }; + })(this)).attr('data-container', 'body').on('click', this.clickDay); + }; + + Calendar.prototype.renderDayTitles = function() { + var days; + days = [ + { + text: 'M', + y: 29 + (this.daySizeWithSpace * 1) + }, { + text: 'W', + y: 29 + (this.daySizeWithSpace * 3) + }, { + text: 'F', + y: 29 + (this.daySizeWithSpace * 5) + } + ]; + return this.svg.append('g').selectAll('text').data(days).enter().append('text').attr('text-anchor', 'middle').attr('x', 8).attr('y', function(day) { + return day.y; + }).text(function(day) { + return day.text; + }).attr('class', 'user-contrib-text'); + }; + + Calendar.prototype.renderMonths = function() { + return this.svg.append('g').selectAll('text').data(this.months).enter().append('text').attr('x', function(date) { + return date.x; + }).attr('y', 10).attr('class', 'user-contrib-text').text((function(_this) { + return function(date) { + return _this.monthNames[date.month]; + }; + })(this)); + }; + + Calendar.prototype.renderKey = function() { + var keyColors; + keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; + return this.svg.append('g').attr('transform', "translate(18, " + (this.daySizeWithSpace * 8 + 16) + ")").selectAll('rect').data(keyColors).enter().append('rect').attr('width', this.daySize).attr('height', this.daySize).attr('x', (function(_this) { + return function(color, i) { + return _this.daySizeWithSpace * i; + }; + })(this)).attr('y', 0).attr('fill', function(color) { + return color; + }); + }; + + Calendar.prototype.initColor = function() { + var colorRange; + colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; + return d3.scale.threshold().domain([0, 10, 20, 30]).range(colorRange); + }; + + Calendar.prototype.initColorKey = function() { + return d3.scale.linear().range(['#acd5f2', '#254e77']).domain([0, 3]); + }; + + Calendar.prototype.clickDay = function(stamp) { + var formatted_date; + if (this.currentSelectedDate !== stamp.date) { + this.currentSelectedDate = stamp.date; + formatted_date = this.currentSelectedDate.getFullYear() + "-" + (this.currentSelectedDate.getMonth() + 1) + "-" + this.currentSelectedDate.getDate(); + return $.ajax({ + url: this.calendar_activities_path, + data: { + date: formatted_date + }, + cache: false, + dataType: 'html', + beforeSend: function() { + return $('.user-calendar-activities').html('<div class="text-center"><i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i></div>'); + }, + success: function(data) { + return $('.user-calendar-activities').html(data); + } + }); + } else { + return $('.user-calendar-activities').html(''); + } + }; + + Calendar.prototype.initTooltips = function() { + return $('.js-contrib-calendar .js-tooltip').tooltip({ + html: true + }); + }; + + return Calendar; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/users/calendar.js.coffee b/app/assets/javascripts/users/calendar.js.coffee deleted file mode 100644 index c49ba5186f2..00000000000 --- a/app/assets/javascripts/users/calendar.js.coffee +++ /dev/null @@ -1,194 +0,0 @@ -class @Calendar - constructor: (timestamps, @calendar_activities_path) -> - @currentSelectedDate = '' - @daySpace = 1 - @daySize = 15 - @daySizeWithSpace = @daySize + (@daySpace * 2) - @monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] - @months = [] - - # Loop through the timestamps to create a group of objects - # The group of objects will be grouped based on the day of the week they are - @timestampsTmp = [] - i = 0 - group = 0 - _.each timestamps, (count, date) => - newDate = new Date parseInt(date) * 1000 - day = newDate.getDay() - - # Create a new group array if this is the first day of the week - # or if is first object - if (day is 0 and i isnt 0) or i is 0 - @timestampsTmp.push [] - group++ - - innerArray = @timestampsTmp[group-1] - - # Push to the inner array the values that will be used to render map - innerArray.push - count: count - date: newDate - day: day - - i++ - - # Init color functions - @colorKey = @initColorKey() - @color = @initColor() - - # Init the svg element - @renderSvg(group) - @renderDays() - @renderMonths() - @renderDayTitles() - @renderKey() - - @initTooltips() - - renderSvg: (group) -> - @svg = d3.select '.js-contrib-calendar' - .append 'svg' - .attr 'width', (group + 1) * @daySizeWithSpace - .attr 'height', 167 - .attr 'class', 'contrib-calendar' - - renderDays: -> - @svg.selectAll 'g' - .data @timestampsTmp - .enter() - .append 'g' - .attr 'transform', (group, i) => - _.each group, (stamp, a) => - if a is 0 and stamp.day is 0 - month = stamp.date.getMonth() - x = (@daySizeWithSpace * i + 1) + @daySizeWithSpace - lastMonth = _.last(@months) - if lastMonth? - lastMonthX = lastMonth.x - - if !lastMonth? - @months.push - month: month - x: x - else if month isnt lastMonth.month and x - @daySizeWithSpace isnt lastMonthX - @months.push - month: month - x: x - - "translate(#{(@daySizeWithSpace * i + 1) + @daySizeWithSpace}, 18)" - .selectAll 'rect' - .data (stamp) -> - stamp - .enter() - .append 'rect' - .attr 'x', '0' - .attr 'y', (stamp, i) => - (@daySizeWithSpace * stamp.day) - .attr 'width', @daySize - .attr 'height', @daySize - .attr 'title', (stamp) => - date = new Date(stamp.date) - contribText = 'No contributions' - - if stamp.count > 0 - contribText = "#{stamp.count} contribution#{if stamp.count > 1 then 's' else ''}" - - dateText = dateFormat(date, 'mmm d, yyyy') - - "#{contribText}<br />#{gl.utils.getDayName(date)} #{dateText}" - .attr 'class', 'user-contrib-cell js-tooltip' - .attr 'fill', (stamp) => - if stamp.count isnt 0 - @color(Math.min(stamp.count, 40)) - else - '#ededed' - .attr 'data-container', 'body' - .on 'click', @clickDay - - renderDayTitles: -> - days = [{ - text: 'M' - y: 29 + (@daySizeWithSpace * 1) - }, { - text: 'W' - y: 29 + (@daySizeWithSpace * 3) - }, { - text: 'F' - y: 29 + (@daySizeWithSpace * 5) - }] - @svg.append 'g' - .selectAll 'text' - .data days - .enter() - .append 'text' - .attr 'text-anchor', 'middle' - .attr 'x', 8 - .attr 'y', (day) -> - day.y - .text (day) -> - day.text - .attr 'class', 'user-contrib-text' - - renderMonths: -> - @svg.append 'g' - .selectAll 'text' - .data @months - .enter() - .append 'text' - .attr 'x', (date) -> - date.x - .attr 'y', 10 - .attr 'class', 'user-contrib-text' - .text (date) => - @monthNames[date.month] - - renderKey: -> - keyColors = ['#ededed', @colorKey(0), @colorKey(1), @colorKey(2), @colorKey(3)] - @svg.append 'g' - .attr 'transform', "translate(18, #{@daySizeWithSpace * 8 + 16})" - .selectAll 'rect' - .data keyColors - .enter() - .append 'rect' - .attr 'width', @daySize - .attr 'height', @daySize - .attr 'x', (color, i) => - @daySizeWithSpace * i - .attr 'y', 0 - .attr 'fill', (color) -> - color - - initColor: -> - colorRange = ['#ededed', @colorKey(0), @colorKey(1), @colorKey(2), @colorKey(3)] - d3.scale - .threshold() - .domain([0, 10, 20, 30]) - .range(colorRange) - - initColorKey: -> - d3.scale - .linear() - .range(['#acd5f2', '#254e77']) - .domain([0, 3]) - - clickDay: (stamp) => - if @currentSelectedDate isnt stamp.date - @currentSelectedDate = stamp.date - formatted_date = @currentSelectedDate.getFullYear() + "-" + (@currentSelectedDate.getMonth()+1) + "-" + @currentSelectedDate.getDate() - - $.ajax - url: @calendar_activities_path - data: - date: formatted_date - cache: false - dataType: 'html' - beforeSend: -> - $('.user-calendar-activities').html '<div class="text-center"><i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i></div>' - success: (data) -> - $('.user-calendar-activities').html data - else - $('.user-calendar-activities').html '' - - initTooltips: -> - $('.js-contrib-calendar .js-tooltip').tooltip - html: true diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js new file mode 100644 index 00000000000..b95faadc8e7 --- /dev/null +++ b/app/assets/javascripts/users/users_bundle.js @@ -0,0 +1,7 @@ + +/*= require_tree . */ + +(function() { + + +}).call(this); diff --git a/app/assets/javascripts/users/users_bundle.js.coffee b/app/assets/javascripts/users/users_bundle.js.coffee deleted file mode 100644 index 91cacfece46..00000000000 --- a/app/assets/javascripts/users/users_bundle.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -# -#= require_tree . diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js new file mode 100644 index 00000000000..4af2a214e12 --- /dev/null +++ b/app/assets/javascripts/users_select.js @@ -0,0 +1,344 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + slice = [].slice; + + this.UsersSelect = (function() { + function UsersSelect(currentUser) { + this.users = bind(this.users, this); + this.user = bind(this.user, this); + this.usersPath = "/autocomplete/users.json"; + this.userPath = "/autocomplete/users/:id.json"; + if (currentUser != null) { + this.currentUser = JSON.parse(currentUser); + } + $('.js-user-search').each((function(_this) { + return function(i, dropdown) { + var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser; + $dropdown = $(dropdown); + _this.projectId = $dropdown.data('project-id'); + _this.showCurrentUser = $dropdown.data('current-user'); + showNullUser = $dropdown.data('null-user'); + showAnyUser = $dropdown.data('any-user'); + firstUser = $dropdown.data('first-user'); + _this.authorId = $dropdown.data('author-id'); + selectedId = $dropdown.data('selected'); + defaultLabel = $dropdown.data('default-label'); + issueURL = $dropdown.data('issueUpdate'); + $selectbox = $dropdown.closest('.selectbox'); + $block = $selectbox.closest('.block'); + abilityName = $dropdown.data('ability-name'); + $value = $block.find('.value'); + $collapsedSidebar = $block.find('.sidebar-collapsed-user'); + $loading = $block.find('.block-loading').fadeOut(); + $block.on('click', '.js-assign-yourself', function(e) { + e.preventDefault(); + return assignTo(_this.currentUser.id); + }); + assignTo = function(selected) { + var data; + data = {}; + data[abilityName] = {}; + data[abilityName].assignee_id = selected != null ? selected : null; + $loading.fadeIn(); + $dropdown.trigger('loading.gl.dropdown'); + return $.ajax({ + type: 'PUT', + dataType: 'json', + url: issueURL, + data: data + }).done(function(data) { + var user; + $dropdown.trigger('loaded.gl.dropdown'); + $loading.fadeOut(); + $selectbox.hide(); + if (data.assignee) { + user = { + name: data.assignee.name, + username: data.assignee.username, + avatar: data.assignee.avatar_url + }; + } else { + user = { + name: 'Unassigned', + username: '', + avatar: '' + }; + } + $value.html(assigneeTemplate(user)); + $collapsedSidebar.attr('title', user.name).tooltip('fixTitle'); + return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); + }); + }; + collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/u/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>'); + assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/u/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>'); + return $dropdown.glDropdown({ + data: function(term, callback) { + var isAuthorFilter; + isAuthorFilter = $('.js-author-search'); + return _this.users(term, function(users) { + var anyUser, index, j, len, name, obj, showDivider; + if (term.length === 0) { + showDivider = 0; + if (firstUser) { + for (index = j = 0, len = users.length; j < len; index = ++j) { + obj = users[index]; + if (obj.username === firstUser) { + users.splice(index, 1); + users.unshift(obj); + break; + } + } + } + if (showNullUser) { + showDivider += 1; + users.unshift({ + beforeDivider: true, + name: 'Unassigned', + id: 0 + }); + } + if (showAnyUser) { + showDivider += 1; + name = showAnyUser; + if (name === true) { + name = 'Any User'; + } + anyUser = { + beforeDivider: true, + name: name, + id: null + }; + users.unshift(anyUser); + } + } + if (showDivider) { + users.splice(showDivider, 0, "divider"); + } + return callback(users); + }); + }, + filterable: true, + filterRemote: true, + search: { + fields: ['name', 'username'] + }, + selectable: true, + fieldName: $dropdown.data('field-name'), + toggleLabel: function(selected) { + if (selected && 'id' in selected) { + if (selected.text) { + return selected.text; + } else { + return selected.name; + } + } else { + return defaultLabel; + } + }, + inputId: 'issue_assignee_id', + hidden: function(e) { + $selectbox.hide(); + return $value.css('display', ''); + }, + clicked: function(user) { + var isIssueIndex, isMRIndex, page, selected; + page = $('body').data('page'); + isIssueIndex = page === 'projects:issues:index'; + isMRIndex = (page === page && page === 'projects:merge_requests:index'); + if ($dropdown.hasClass('js-filter-bulk-update')) { + return; + } + if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { + selectedId = user.id; + return Issuable.filterResults($dropdown.closest('form')); + } else if ($dropdown.hasClass('js-filter-submit')) { + return $dropdown.closest('form').submit(); + } else { + selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val(); + return assignTo(selected); + } + }, + renderRow: function(user) { + var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username; + username = user.username ? "@" + user.username : ""; + avatar = user.avatar_url ? user.avatar_url : false; + selected = user.id === selectedId ? "is-active" : ""; + img = ""; + if (user.beforeDivider != null) { + "<li> <a href='#' class='" + selected + "'> " + user.name + " </a> </li>"; + } else { + if (avatar) { + img = "<img src='" + avatar + "' class='avatar avatar-inline' width='30' />"; + } + } + listWithName = "<li> <a href='#' class='dropdown-menu-user-link " + selected + "'> " + img + " <strong class='dropdown-menu-user-full-name'> " + user.name + " </strong>"; + listWithUserName = "<span class='dropdown-menu-user-username'> " + username + " </span>"; + listClosingTags = "</a> </li>"; + if (username === '') { + listWithUserName = ''; + } + return listWithName + listWithUserName + listClosingTags; + } + }); + }; + })(this)); + $('.ajax-users-select').each((function(_this) { + return function(i, select) { + var firstUser, showAnyUser, showEmailUser, showNullUser; + _this.projectId = $(select).data('project-id'); + _this.groupId = $(select).data('group-id'); + _this.showCurrentUser = $(select).data('current-user'); + _this.authorId = $(select).data('author-id'); + _this.skipUsers = $(select).data('skip-users'); + showNullUser = $(select).data('null-user'); + showAnyUser = $(select).data('any-user'); + showEmailUser = $(select).data('email-user'); + firstUser = $(select).data('first-user'); + return $(select).select2({ + placeholder: "Search for a user", + multiple: $(select).hasClass('multiselect'), + minimumInputLength: 0, + query: function(query) { + return _this.users(query.term, function(users) { + var anyUser, data, emailUser, index, j, len, name, nullUser, obj, ref; + data = { + results: users + }; + if (query.term.length === 0) { + if (firstUser) { + ref = data.results; + for (index = j = 0, len = ref.length; j < len; index = ++j) { + obj = ref[index]; + if (obj.username === firstUser) { + data.results.splice(index, 1); + data.results.unshift(obj); + break; + } + } + } + if (showNullUser) { + nullUser = { + name: 'Unassigned', + id: 0 + }; + data.results.unshift(nullUser); + } + if (showAnyUser) { + name = showAnyUser; + if (name === true) { + name = 'Any User'; + } + anyUser = { + name: name, + id: null + }; + data.results.unshift(anyUser); + } + } + if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) { + emailUser = { + name: "Invite \"" + query.term + "\"", + username: query.term, + id: query.term + }; + data.results.unshift(emailUser); + } + return query.callback(data); + }); + }, + initSelection: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return _this.initSelection.apply(_this, args); + }, + formatResult: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return _this.formatResult.apply(_this, args); + }, + formatSelection: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return _this.formatSelection.apply(_this, args); + }, + dropdownCssClass: "ajax-users-dropdown", + escapeMarkup: function(m) { + return m; + } + }); + }; + })(this)); + } + + UsersSelect.prototype.initSelection = function(element, callback) { + var id, nullUser; + id = $(element).val(); + if (id === "0") { + nullUser = { + name: 'Unassigned' + }; + return callback(nullUser); + } else if (id !== "") { + return this.user(id, callback); + } + }; + + UsersSelect.prototype.formatResult = function(user) { + var avatar; + if (user.avatar_url) { + avatar = user.avatar_url; + } else { + avatar = gon.default_avatar_url; + } + return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar s24' src='" + avatar + "'></div> <div class='user-name'>" + user.name + "</div> <div class='user-username'>" + (user.username || "") + "</div> </div>"; + }; + + UsersSelect.prototype.formatSelection = function(user) { + return user.name; + }; + + UsersSelect.prototype.user = function(user_id, callback) { + var url; + url = this.buildUrl(this.userPath); + url = url.replace(':id', user_id); + return $.ajax({ + url: url, + dataType: "json" + }).done(function(user) { + return callback(user); + }); + }; + + UsersSelect.prototype.users = function(query, callback) { + var url; + url = this.buildUrl(this.usersPath); + return $.ajax({ + url: url, + data: { + search: query, + per_page: 20, + active: true, + project_id: this.projectId, + group_id: this.groupId, + current_user: this.showCurrentUser, + author_id: this.authorId, + skip_users: this.skipUsers + }, + dataType: "json" + }).done(function(users) { + return callback(users); + }); + }; + + UsersSelect.prototype.buildUrl = function(url) { + if (gon.relative_url_root != null) { + url = gon.relative_url_root.replace(/\/$/, '') + url; + } + return url; + }; + + return UsersSelect; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee deleted file mode 100644 index 344be811e0d..00000000000 --- a/app/assets/javascripts/users_select.js.coffee +++ /dev/null @@ -1,330 +0,0 @@ -class @UsersSelect - constructor: (currentUser) -> - @usersPath = "/autocomplete/users.json" - @userPath = "/autocomplete/users/:id.json" - if currentUser? - @currentUser = JSON.parse(currentUser) - - $('.js-user-search').each (i, dropdown) => - $dropdown = $(dropdown) - @projectId = $dropdown.data('project-id') - @showCurrentUser = $dropdown.data('current-user') - showNullUser = $dropdown.data('null-user') - showAnyUser = $dropdown.data('any-user') - firstUser = $dropdown.data('first-user') - @authorId = $dropdown.data('author-id') - selectedId = $dropdown.data('selected') - defaultLabel = $dropdown.data('default-label') - issueURL = $dropdown.data('issueUpdate') - $selectbox = $dropdown.closest('.selectbox') - $block = $selectbox.closest('.block') - abilityName = $dropdown.data('ability-name') - $value = $block.find('.value') - $collapsedSidebar = $block.find('.sidebar-collapsed-user') - $loading = $block.find('.block-loading').fadeOut() - - $block.on('click', '.js-assign-yourself', (e) => - e.preventDefault() - assignTo(@currentUser.id) - ) - - assignTo = (selected) -> - data = {} - data[abilityName] = {} - data[abilityName].assignee_id = if selected? then selected else null - $loading - .fadeIn() - $dropdown.trigger('loading.gl.dropdown') - $.ajax( - type: 'PUT' - dataType: 'json' - url: issueURL - data: data - ).done (data) -> - $dropdown.trigger('loaded.gl.dropdown') - $loading.fadeOut() - $selectbox.hide() - - if data.assignee - user = - name: data.assignee.name - username: data.assignee.username - avatar: data.assignee.avatar_url - else - user = - name: 'Unassigned' - username: '' - avatar: '' - $value.html(assigneeTemplate(user)) - - $collapsedSidebar - .attr('title', user.name) - .tooltip('fixTitle') - - $collapsedSidebar.html(collapsedAssigneeTemplate(user)) - - - collapsedAssigneeTemplate = _.template( - '<% if( avatar ) { %> - <a class="author_link" href="/u/<%- username %>"> - <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> - </a> - <% } else { %> - <i class="fa fa-user"></i> - <% } %>' - ) - - assigneeTemplate = _.template( - '<% if (username) { %> - <a class="author_link bold" href="/u/<%- username %>"> - <% if( avatar ) { %> - <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> - <% } %> - <span class="author"><%- name %></span> - <span class="username"> - @<%- username %> - </span> - </a> - <% } else { %> - <span class="no-value assign-yourself"> - No assignee - - <a href="#" class="js-assign-yourself"> - assign yourself - </a> - </span> - <% } %>' - ) - - $dropdown.glDropdown( - data: (term, callback) => - isAuthorFilter = $('.js-author-search') - - @users term, (users) => - if term.length is 0 - showDivider = 0 - - if firstUser - # Move current user to the front of the list - for obj, index in users - if obj.username == firstUser - users.splice(index, 1) - users.unshift(obj) - break - - if showNullUser - showDivider += 1 - users.unshift( - beforeDivider: true - name: 'Unassigned', - id: 0 - ) - - if showAnyUser - showDivider += 1 - name = showAnyUser - name = 'Any User' if name == true - anyUser = { - beforeDivider: true - name: name, - id: null - } - users.unshift(anyUser) - - if showDivider - users.splice(showDivider, 0, "divider") - - # Send the data back - callback users - filterable: true - filterRemote: true - search: - fields: ['name', 'username'] - selectable: true - fieldName: $dropdown.data('field-name') - - toggleLabel: (selected) -> - if selected && 'id' of selected - if selected.text then selected.text else selected.name - else - defaultLabel - - inputId: 'issue_assignee_id' - - hidden: (e) -> - $selectbox.hide() - # display:block overrides the hide-collapse rule - $value.css('display', '') - - clicked: (user) -> - page = $('body').data 'page' - isIssueIndex = page is 'projects:issues:index' - isMRIndex = page is page is 'projects:merge_requests:index' - if $dropdown.hasClass('js-filter-bulk-update') - return - - if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) - selectedId = user.id - Issuable.filterResults $dropdown.closest('form') - else if $dropdown.hasClass 'js-filter-submit' - $dropdown.closest('form').submit() - else - selected = $dropdown - .closest('.selectbox') - .find("input[name='#{$dropdown.data('field-name')}']").val() - assignTo(selected) - - renderRow: (user) -> - username = if user.username then "@#{user.username}" else "" - avatar = if user.avatar_url then user.avatar_url else false - selected = if user.id is selectedId then "is-active" else "" - img = "" - - if user.beforeDivider? - "<li> - <a href='#' class='#{selected}'> - #{user.name} - </a> - </li>" - else - if avatar - img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />" - - # split into three parts so we can remove the username section if nessesary - listWithName = "<li> - <a href='#' class='dropdown-menu-user-link #{selected}'> - #{img} - <strong class='dropdown-menu-user-full-name'> - #{user.name} - </strong>" - - listWithUserName = "<span class='dropdown-menu-user-username'> - #{username} - </span>" - listClosingTags = "</a> - </li>" - - - if username is '' - listWithUserName = '' - - listWithName + listWithUserName + listClosingTags - ) - - $('.ajax-users-select').each (i, select) => - @projectId = $(select).data('project-id') - @groupId = $(select).data('group-id') - @showCurrentUser = $(select).data('current-user') - @authorId = $(select).data('author-id') - showNullUser = $(select).data('null-user') - showAnyUser = $(select).data('any-user') - showEmailUser = $(select).data('email-user') - firstUser = $(select).data('first-user') - - $(select).select2 - placeholder: "Search for a user" - multiple: $(select).hasClass('multiselect') - minimumInputLength: 0 - query: (query) => - @users query.term, (users) => - data = { results: users } - - if query.term.length == 0 - if firstUser - # Move current user to the front of the list - for obj, index in data.results - if obj.username == firstUser - data.results.splice(index, 1) - data.results.unshift(obj) - break - - if showNullUser - nullUser = { - name: 'Unassigned', - id: 0 - } - data.results.unshift(nullUser) - - if showAnyUser - name = showAnyUser - name = 'Any User' if name == true - anyUser = { - name: name, - id: null - } - data.results.unshift(anyUser) - - if showEmailUser && data.results.length == 0 && query.term.match(/^[^@]+@[^@]+$/) - emailUser = { - name: "Invite \"#{query.term}\"", - username: query.term, - id: query.term - } - data.results.unshift(emailUser) - - query.callback(data) - - initSelection: (args...) => - @initSelection(args...) - formatResult: (args...) => - @formatResult(args...) - formatSelection: (args...) => - @formatSelection(args...) - dropdownCssClass: "ajax-users-dropdown" - escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results - m - - initSelection: (element, callback) -> - id = $(element).val() - if id == "0" - nullUser = { name: 'Unassigned' } - callback(nullUser) - else if id != "" - @user(id, callback) - - formatResult: (user) -> - if user.avatar_url - avatar = user.avatar_url - else - avatar = gon.default_avatar_url - - "<div class='user-result #{'no-username' unless user.username}'> - <div class='user-image'><img class='avatar s24' src='#{avatar}'></div> - <div class='user-name'>#{user.name}</div> - <div class='user-username'>#{user.username || ""}</div> - </div>" - - formatSelection: (user) -> - user.name - - user: (user_id, callback) => - url = @buildUrl(@userPath) - url = url.replace(':id', user_id) - - $.ajax( - url: url - dataType: "json" - ).done (user) -> - callback(user) - - # Return users list. Filtered by query - # Only active users retrieved - users: (query, callback) => - url = @buildUrl(@usersPath) - - $.ajax( - url: url - data: - search: query - per_page: 20 - active: true - project_id: @projectId - group_id: @groupId - current_user: @showCurrentUser - author_id: @authorId - dataType: "json" - ).done (users) -> - callback(users) - - buildUrl: (url) -> - url = gon.relative_url_root.replace(/\/$/, '') + url if gon.relative_url_root? - return url diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js new file mode 100644 index 00000000000..35401231fbf --- /dev/null +++ b/app/assets/javascripts/wikis.js @@ -0,0 +1,37 @@ + +/*= require latinise */ + +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.Wikis = (function() { + function Wikis() { + this.slugify = bind(this.slugify, this); + $('.new-wiki-page').on('submit', (function(_this) { + return function(e) { + var field, path, slug; + $('[data-error~=slug]').addClass('hidden'); + field = $('#new_wiki_path'); + slug = _this.slugify(field.val()); + if (slug.length > 0) { + path = field.attr('data-wikis-path'); + location.href = path + '/' + slug; + return e.preventDefault(); + } + }; + })(this)); + } + + Wikis.prototype.dasherize = function(value) { + return value.replace(/[_\s]+/g, '-'); + }; + + Wikis.prototype.slugify = function(value) { + return this.dasherize(value.trim().toLowerCase().latinise()); + }; + + return Wikis; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/wikis.js.coffee b/app/assets/javascripts/wikis.js.coffee deleted file mode 100644 index 1ee827f1fa3..00000000000 --- a/app/assets/javascripts/wikis.js.coffee +++ /dev/null @@ -1,19 +0,0 @@ -#= require latinise - -class @Wikis - constructor: -> - $('.new-wiki-page').on 'submit', (e) => - $('[data-error~=slug]').addClass('hidden') - field = $('#new_wiki_path') - slug = @slugify(field.val()) - - if (slug.length > 0) - path = field.attr('data-wikis-path') - location.href = path + '/' + slug - e.preventDefault() - - dasherize: (value) -> - value.replace(/[_\s]+/g, '-') - - slugify: (value) => - @dasherize(value.trim().toLowerCase().latinise()) diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js new file mode 100644 index 00000000000..71236c6a27d --- /dev/null +++ b/app/assets/javascripts/zen_mode.js @@ -0,0 +1,80 @@ + +/*= provides zen_mode:enter */ + + +/*= provides zen_mode:leave */ + + +/*= require jquery.scrollTo */ + + +/*= require dropzone */ + + +/*= require mousetrap */ + + +/*= require mousetrap/pause */ + +(function() { + this.ZenMode = (function() { + function ZenMode() { + this.active_backdrop = null; + this.active_textarea = null; + $(document).on('click', '.js-zen-enter', function(e) { + e.preventDefault(); + return $(e.currentTarget).trigger('zen_mode:enter'); + }); + $(document).on('click', '.js-zen-leave', function(e) { + e.preventDefault(); + return $(e.currentTarget).trigger('zen_mode:leave'); + }); + $(document).on('zen_mode:enter', (function(_this) { + return function(e) { + return _this.enter($(e.target).closest('.md-area').find('.zen-backdrop')); + }; + })(this)); + $(document).on('zen_mode:leave', (function(_this) { + return function(e) { + return _this.exit(); + }; + })(this)); + $(document).on('keydown', function(e) { + if (e.keyCode === 27) { + e.preventDefault(); + return $(document).trigger('zen_mode:leave'); + } + }); + } + + ZenMode.prototype.enter = function(backdrop) { + Mousetrap.pause(); + this.active_backdrop = $(backdrop); + this.active_backdrop.addClass('fullscreen'); + this.active_textarea = this.active_backdrop.find('textarea'); + this.active_textarea.removeAttr('style'); + return this.active_textarea.focus(); + }; + + ZenMode.prototype.exit = function() { + if (this.active_textarea) { + Mousetrap.unpause(); + this.active_textarea.closest('.zen-backdrop').removeClass('fullscreen'); + this.scrollTo(this.active_textarea); + this.active_textarea = null; + this.active_backdrop = null; + return Dropzone.forElement('.div-dropzone').enable(); + } + }; + + ZenMode.prototype.scrollTo = function(zen_area) { + return $.scrollTo(zen_area, 0, { + offset: -150 + }); + }; + + return ZenMode; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/zen_mode.js.coffee b/app/assets/javascripts/zen_mode.js.coffee deleted file mode 100644 index 99f35ecfb0f..00000000000 --- a/app/assets/javascripts/zen_mode.js.coffee +++ /dev/null @@ -1,80 +0,0 @@ -# Zen Mode (full screen) textarea -# -#= provides zen_mode:enter -#= provides zen_mode:leave -# -#= require jquery.scrollTo -#= require dropzone -#= require mousetrap -#= require mousetrap/pause -# -# ### Events -# -# `zen_mode:enter` -# -# Fired when the "Edit in fullscreen" link is clicked. -# -# **Synchronicity** Sync -# **Bubbles** Yes -# **Cancelable** No -# **Target** a.js-zen-enter -# -# `zen_mode:leave` -# -# Fired when the "Leave Fullscreen" link is clicked. -# -# **Synchronicity** Sync -# **Bubbles** Yes -# **Cancelable** No -# **Target** a.js-zen-leave -# -class @ZenMode - constructor: -> - @active_backdrop = null - @active_textarea = null - - $(document).on 'click', '.js-zen-enter', (e) -> - e.preventDefault() - $(e.currentTarget).trigger('zen_mode:enter') - - $(document).on 'click', '.js-zen-leave', (e) -> - e.preventDefault() - $(e.currentTarget).trigger('zen_mode:leave') - - $(document).on 'zen_mode:enter', (e) => - @enter($(e.target).closest('.md-area').find('.zen-backdrop')) - $(document).on 'zen_mode:leave', (e) => - @exit() - - $(document).on 'keydown', (e) -> - if e.keyCode == 27 # Esc - e.preventDefault() - $(document).trigger('zen_mode:leave') - - enter: (backdrop) -> - Mousetrap.pause() - - @active_backdrop = $(backdrop) - @active_backdrop.addClass('fullscreen') - - @active_textarea = @active_backdrop.find('textarea') - - # Prevent a user-resized textarea from persisting to fullscreen - @active_textarea.removeAttr('style') - @active_textarea.focus() - - exit: -> - if @active_textarea - Mousetrap.unpause() - - @active_textarea.closest('.zen-backdrop').removeClass('fullscreen') - - @scrollTo(@active_textarea) - - @active_textarea = null - @active_backdrop = null - - Dropzone.forElement('.div-dropzone').enable() - - scrollTo: (zen_area) -> - $.scrollTo(zen_area, 0, offset: -150) diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 8b6ddf8ba18..c79b22d4d21 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -5,6 +5,7 @@ height: 40px; padding: 0; @include border-radius($avatar_radius); + border: 1px solid rgba(0, 0, 0, .1); &.avatar-inline { float: none; @@ -15,8 +16,9 @@ &.s24 { margin-right: 4px; } } - &.group-avatar, &.project-avatar, &.avatar-tile { + &.avatar-tile { @include border-radius(0); + border: none; } &.s16 { width: 16px; height: 16px; margin-right: 6px; } @@ -43,12 +45,12 @@ &.s16 { font-size: 12px; line-height: 1.33; } &.s24 { font-size: 14px; line-height: 1.8; } &.s26 { font-size: 20px; line-height: 1.33; } - &.s32 { font-size: 20px; line-height: 32px; } - &.s40 { font-size: 16px; line-height: 40px; } - &.s60 { font-size: 32px; line-height: 60px; } - &.s70 { font-size: 34px; line-height: 70px; } - &.s90 { font-size: 36px; line-height: 90px; } - &.s110 { font-size: 40px; line-height: 112px; font-weight: 300; } - &.s140 { font-size: 72px; line-height: 140px; } - &.s160 { font-size: 96px; line-height: 160px; } + &.s32 { font-size: 20px; line-height: 30px; } + &.s40 { font-size: 16px; line-height: 38px; } + &.s60 { font-size: 32px; line-height: 58px; } + &.s70 { font-size: 34px; line-height: 68px; } + &.s90 { font-size: 36px; line-height: 88px; } + &.s110 { font-size: 40px; line-height: 108px; font-weight: 300; } + &.s140 { font-size: 72px; line-height: 138px; } + &.s160 { font-size: 96px; line-height: 158px; } } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index f87b8a2ad1c..473530cf094 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -135,6 +135,15 @@ @include btn-green; } + &.btn-inverted { + &.btn-success, + &.btn-new, + &.btn-create, + &.btn-save { + @include btn-outline($white-light, $green-normal, $green-normal, $green-light, $white-light, $green-light); + } + } + &.btn-gray { @include btn-gray; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index d4e900f80ef..c54eb0d6479 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -350,6 +350,7 @@ .dropdown-input-field, .default-dropdown-input { width: 100%; + min-height: 30px; padding: 0 7px; color: $dropdown-input-color; line-height: 30px; diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 5d3273ea64d..96565da1bc9 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -98,13 +98,30 @@ .md { &.md-preview-holder { - code { - white-space: pre-wrap; - word-break: keep-all; - } - + // Reset ul style types since we're nested inside a ul already @include bulleted-list; } + + // On diffs code should wrap nicely and not overflow + code { + white-space: pre-wrap; + word-break: keep-all; + } + + hr { + // Darken 'whitesmoke' a bit to make it more visible in note bodies + border-color: darken(#f5f5f5, 8%); + margin: 10px 0; + } + + // Border around images in issue and MR comments. + img:not(.emoji) { + border: 1px solid $table-border-gray; + padding: 5px; + margin: 5px 0; + // Ensure that image does not exceed viewport + max-height: calc(100vh - 100px); + } } .toolbar-group { diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss index 7f645d3089d..33aedf1f7c1 100644 --- a/app/assets/stylesheets/mailers/repository_push_email.scss +++ b/app/assets/stylesheets/mailers/repository_push_email.scss @@ -10,83 +10,48 @@ // preference): plain class selectors, type (element name) selectors, or // explicit child selectors. -table.code { - width: 100%; +.code { + background-color: #fff; font-family: monospace; - border: none; - border-collapse: separate; - margin: 0; - padding: 0; + font-size: $code_font_size; -premailer-cellpadding: 0; -premailer-cellspacing: 0; -premailer-width: 100%; - > tr > td { + > tr { line-height: $code_line_height; - font-family: monospace; - font-size: $code_font_size; - - &.diff-line-num { - margin: 0; - padding: 0; - border: none; - padding: 0 5px; - border-right: 1px solid; - text-align: right; - min-width: 35px; - max-width: 50px; - width: 35px; - } - - &.line_content { - display: block; - margin: 0; - padding: 0 0.5em; - border: none; - white-space: pre; - } } } -.line-numbers, .diff-line-num { +.diff-line-num { + padding: 0 5px; + text-align: right; + width: 35px; background-color: $background-color; -} - -.diff-line-num, .diff-line-num a { color: $black-transparent; -} - -pre.code, .diff-line-num { - border-color: $table-border-gray; -} + border-right: 1px solid $table-border-gray; -.code.white, pre.code, .line_content { - background-color: #fff; - color: #333; -} - -.diff-line-num { &.old { background-color: $line-number-old; - border-color: $line-removed-dark; + border-right-color: $line-removed-dark; } &.new { background-color: $line-number-new; - border-color: $line-added-dark; - } - - &.hll:not(.empty-cell) { - background-color: $line-number-select; - border-color: $line-select-yellow-dark; + border-right-color: $line-added-dark; } } .line_content { + padding-left: 0.5em; + padding-right: 0.5em; + white-space: pre; + &.old { background-color: $line-removed; - > .line > span.idiff, > .line > span > span.idiff { + > .line > span.idiff, + > .line > span > span.idiff { background-color: $line-removed-dark; } } @@ -94,7 +59,8 @@ pre.code, .diff-line-num { &.new { background-color: $line-added; - > .line > span.idiff, > .line > span > span.idiff { + > .line > span.idiff, + > .line > span > span.idiff { background-color: $line-added-dark; } } @@ -103,14 +69,6 @@ pre.code, .diff-line-num { color: $black-transparent; background-color: $match-line; } - - &.hll:not(.empty-cell) { - background-color: $line-select-yellow; - } -} - -pre > .hll { - background-color: #f8eec7 !important; } span.highlight_word { diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 99a2cd306cf..e26f8f7080d 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -53,6 +53,14 @@ left: 70px; } } + + .nav-links { + svg { + position: relative; + top: 2px; + margin-right: 3px; + } + } } .build-header { diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 35ab28b3fea..bbe0c6c5f1f 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -68,6 +68,12 @@ } } +.ci-status-link { + svg { + overflow: visible; + } +} + .commit-box { border-top: 1px solid $border-color; diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss index cf7567513ec..42928ee279c 100644 --- a/app/assets/stylesheets/pages/dashboard.scss +++ b/app/assets/stylesheets/pages/dashboard.scss @@ -36,10 +36,6 @@ .dash-project-avatar { float: left; - - .avatar { - @include border-radius(50%); - } } .dash-project-access-icon { diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index a2145956eb5..5c336bb1c7e 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -176,3 +176,11 @@ } } } + +// hide event scope (namespace + project) where it is not necessary +.project-activity { + .event-scope { + display: none; + } +} + diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index ded437625ee..7a50bc9c832 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -269,7 +269,7 @@ .issuable-header-btn { background: $gray-normal; border: 1px solid $border-gray-normal; - + &:hover { background: $gray-dark; border: 1px solid $border-gray-dark; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 4e806e60e7e..0a661e529f0 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -64,6 +64,7 @@ margin-right: 4px; position: relative; top: 1px; + overflow: visible; } &.ci-success { @@ -215,6 +216,11 @@ position: relative; top: 3px; } + + &:hover, + &:focus { + text-decoration: none; + } } } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index ac8c02b59dc..a2b5437e503 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -91,34 +91,11 @@ ul.notes { // Reset ul style types since we're nested inside a ul already @include bulleted-list; - // On diffs code should wrap nicely and not overflow - code { - white-space: pre-wrap; - } - ul.task-list { ul:not(.task-list) { padding-left: 1.3em; } } - - hr { - // Darken 'whitesmoke' a bit to make it more visible in note bodies - border-color: darken(#f5f5f5, 8%); - margin: 10px 0; - } - - code { - word-break: keep-all; - } - - // Border around images in issue and MR comments. - img:not(.emoji) { - border: 1px solid $table-border-gray; - padding: 5px; - margin: 5px 0; - max-height: calc(100vh - 100px); - } } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index a404f108dc4..c58e2ffe7f5 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -29,9 +29,18 @@ } } +.pipeline-holder { + width: 100%; + overflow: auto; +} + .table.builds { min-width: 1200px; + &.pipeline { + min-width: 650px; + } + tr { th { padding: 16px 8px; @@ -76,7 +85,7 @@ svg { height: 14px; - width: auto; + width: 14px; vertical-align: middle; fill: $table-text-gray; } @@ -93,7 +102,7 @@ .commit-title { margin-top: 4px; - max-width: 320px; + max-width: 300px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; @@ -138,6 +147,11 @@ height: 18px; width: 18px; vertical-align: middle; + overflow: visible; + } + + .light { + width: 3px; } } @@ -153,7 +167,7 @@ svg { width: 12px; - height: auto; + height: 12px; vertical-align: middle; margin-right: 4px; } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 99f64b5e4cc..587f2d9f3c1 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -49,6 +49,7 @@ position: relative; top: 1px; margin: 0 3px; + overflow: visible; } } @@ -74,3 +75,11 @@ color: $gl-gray; } } + +.visible-xs-inline { + .ci-status-link { + position: relative; + top: 2px; + left: 5px; + } +} diff --git a/app/controllers/admin/requests_profiles_controller.rb b/app/controllers/admin/requests_profiles_controller.rb new file mode 100644 index 00000000000..a478176e138 --- /dev/null +++ b/app/controllers/admin/requests_profiles_controller.rb @@ -0,0 +1,17 @@ +class Admin::RequestsProfilesController < Admin::ApplicationController + def index + @profile_token = Gitlab::RequestProfiler.profile_token + @profiles = Gitlab::RequestProfiler::Profile.all.group_by(&:request_path) + end + + def show + clean_name = Rack::Utils.clean_path_info(params[:name]) + profile = Gitlab::RequestProfiler::Profile.find(clean_name) + + if profile + render text: profile.content + else + redirect_to admin_requests_profiles_path, alert: 'Profile not found' + end + end +end diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index c89678cf2d8..d828d163c28 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -5,6 +5,7 @@ class AutocompleteController < ApplicationController def users @users ||= User.none @users = @users.search(params[:search]) if params[:search].present? + @users = @users.where.not(id: params[:skip_users]) if params[:skip_users].present? @users = @users.active @users = @users.reorder(:name) @users = @users.page(params[:page]) diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index d3dd98c8a4e..f7b44099b78 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -30,7 +30,7 @@ class HelpController < ApplicationController end # Allow access to images in the doc folder - format.any(:png, :gif, :jpeg) do + format.any(:png, :gif, :jpeg, :mp4) do # Note: We are purposefully NOT using `Rails.root.join` path = File.join(Rails.root, 'doc', "#{@path}.#{params[:format]}") diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index dd9508da049..6126acccaab 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -6,8 +6,7 @@ class Projects::BranchesController < Projects::ApplicationController before_action :authorize_push_code!, only: [:new, :create, :destroy] def index - @sort = params[:sort] || 'name' - @branches = @repository.branches_sorted_by(@sort) + @branches = BranchesFinder.new(@repository, params).execute @branches = Kaminari.paginate_array(@branches).page(params[:page]) @max_commits = @branches.reduce(0) do |memo, branch| diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 727e84b40a1..7ae034f9398 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -115,11 +115,11 @@ class Projects::CommitController < Projects::ApplicationController end def define_note_vars - @grouped_diff_notes = commit.notes.grouped_diff_notes + @grouped_diff_discussions = commit.notes.grouped_diff_discussions @notes = commit.notes.non_diff_notes.fresh Banzai::NoteRenderer.render( - @grouped_diff_notes.values.flatten + @notes, + @grouped_diff_discussions.values.flat_map(&:notes) + @notes, @project, current_user, ) diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 10749d0fef8..8c004724f02 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -54,7 +54,7 @@ class Projects::CompareController < Projects::ApplicationController ) @diff_notes_disabled = true - @grouped_diff_notes = {} + @grouped_diff_discussions = {} end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index fa663c9bda4..91ff9407216 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -1,4 +1,5 @@ class Projects::IssuesController < Projects::ApplicationController + include NotesHelper include ToggleSubscriptionAction include IssuableActions include ToggleAwardEmoji @@ -70,6 +71,8 @@ class Projects::IssuesController < Projects::ApplicationController @note = @project.notes.new(noteable: @issue) @noteable = @issue + preload_max_access_for_authors(@notes, @project) + respond_to do |format| format.html format.json do diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 7beeb7d97d0..23252fa59cc 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -3,6 +3,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController include DiffForPath include DiffHelper include IssuableActions + include NotesHelper include ToggleAwardEmoji before_action :module_enabled @@ -97,7 +98,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController else build_merge_request @diff_notes_disabled = true - @grouped_diff_notes = {} + @grouped_diff_discussions = {} end define_commit_vars @@ -378,13 +379,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController # This is not executed lazily @notes = Banzai::NoteRenderer.render( - @discussions.flatten, + @discussions.flat_map(&:notes), @project, current_user, @path, @project_wiki, @ref ) + + preload_max_access_for_authors(@notes, @project) end def define_widget_vars @@ -404,10 +407,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController } @use_legacy_diff_notes = !@merge_request.support_new_diff_notes? - @grouped_diff_notes = @merge_request.notes.grouped_diff_notes + @grouped_diff_discussions = @merge_request.notes.grouped_diff_discussions Banzai::NoteRenderer.render( - @grouped_diff_notes.values.flatten, + @grouped_diff_discussions.values.flat_map(&:notes), @project, current_user, @path, diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 3eacdbbd067..766b7e9cf22 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -73,7 +73,7 @@ class Projects::NotesController < Projects::ApplicationController end alias_method :awardable, :note - def note_to_html(note) + def note_html(note) render_to_string( "projects/notes/_note", layout: false, @@ -82,20 +82,20 @@ class Projects::NotesController < Projects::ApplicationController ) end - def note_to_discussion_html(note) - return unless note.diff_note? + def diff_discussion_html(discussion) + return unless discussion.diff_discussion? if params[:view] == 'parallel' - template = "projects/notes/_diff_notes_with_reply_parallel" + template = "discussions/_parallel_diff_discussion" locals = if params[:line_type] == 'old' - { notes_left: [note], notes_right: [] } + { discussion_left: discussion, discussion_right: nil } else - { notes_left: [], notes_right: [note] } + { discussion_left: nil, discussion_right: discussion } end else - template = "projects/notes/_diff_notes_with_reply" - locals = { notes: [note] } + template = "discussions/_diff_discussion" + locals = { discussion: discussion } end render_to_string( @@ -106,14 +106,14 @@ class Projects::NotesController < Projects::ApplicationController ) end - def note_to_discussion_with_diff_html(note) - return unless note.diff_note? + def discussion_html(discussion) + return unless discussion.diff_discussion? render_to_string( - "projects/notes/_discussion", + "discussions/_discussion", layout: false, formats: [:html], - locals: { discussion_notes: [note] } + locals: { discussion: discussion } ) end @@ -132,26 +132,33 @@ class Projects::NotesController < Projects::ApplicationController valid: true, id: note.id, discussion_id: note.discussion_id, - html: note_to_html(note), + html: note_html(note), award: false, - note: note.note, - discussion_html: note_to_discussion_html(note), - discussion_with_diff_html: note_to_discussion_with_diff_html(note) + note: note.note } - # The discussion_id is used to add the comment to the correct discussion - # element on the merge request page. Among other things, the discussion_id - # contains the sha of head commit of the merge request. - # When new commits are pushed into the merge request after the initial - # load of the merge request page, the discussion elements will still have - # the old discussion_ids, with the old head commit sha. The new comment, - # however, will have the new discussion_id with the new commit sha. - # To ensure that these new comments will still end up in the correct - # discussion element, we also send the original discussion_id, with the - # old commit sha, along, and fall back on this value when no discussion - # element with the new discussion_id could be found. - if note.new_diff_note? && note.position != note.original_position - attrs[:original_discussion_id] = note.original_discussion_id + if note.diff_note? + discussion = Discussion.new([note]) + + attrs.merge!( + diff_discussion_html: diff_discussion_html(discussion), + discussion_html: discussion_html(discussion) + ) + + # The discussion_id is used to add the comment to the correct discussion + # element on the merge request page. Among other things, the discussion_id + # contains the sha of head commit of the merge request. + # When new commits are pushed into the merge request after the initial + # load of the merge request page, the discussion elements will still have + # the old discussion_ids, with the old head commit sha. The new comment, + # however, will have the new discussion_id with the new commit sha. + # To ensure that these new comments will still end up in the correct + # discussion element, we also send the original discussion_id, with the + # old commit sha, along, and fall back on this value when no discussion + # element with the new discussion_id could be found. + if note.new_diff_note? && note.position != note.original_position + attrs[:original_discussion_id] = note.original_discussion_id + end end attrs diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 6dc495247c8..8592579abbd 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -10,11 +10,12 @@ class Projects::TagsController < Projects::ApplicationController @tags = @repository.tags_sorted_by(@sort) @tags = Kaminari.paginate_array(@tags).page(params[:page]) - @releases = project.releases.where(tag: @tags) + @releases = project.releases.where(tag: @tags.map(&:name)) end def show @tag = @repository.find_tag(params[:id]) + @release = @project.releases.find_or_initialize_by(tag: @tag.name) @commit = @repository.commit(@tag.target) end diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb index caed064dfbc..e617be8f9fb 100644 --- a/app/controllers/projects/uploads_controller.rb +++ b/app/controllers/projects/uploads_controller.rb @@ -1,6 +1,6 @@ class Projects::UploadsController < Projects::ApplicationController skip_before_action :reject_blocked!, :project, - :repository, if: -> { action_name == 'show' && image? } + :repository, if: -> { action_name == 'show' && image_or_video? } before_action :authorize_upload_file!, only: [:create] @@ -24,7 +24,7 @@ class Projects::UploadsController < Projects::ApplicationController def show return render_404 if uploader.nil? || !uploader.file.exists? - disposition = uploader.image? ? 'inline' : 'attachment' + disposition = uploader.image_or_video? ? 'inline' : 'attachment' send_file uploader.file.path, disposition: disposition end @@ -49,7 +49,7 @@ class Projects::UploadsController < Projects::ApplicationController @uploader end - def image? - uploader && uploader.file.exists? && uploader.image? + def image_or_video? + uploader && uploader.file.exists? && uploader.image_or_video? end end diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb new file mode 100644 index 00000000000..533076585c0 --- /dev/null +++ b/app/finders/branches_finder.rb @@ -0,0 +1,31 @@ +class BranchesFinder + def initialize(repository, params) + @repository = repository + @params = params + end + + def execute + branches = @repository.branches_sorted_by(sort) + filter_by_name(branches) + end + + private + + attr_reader :repository, :params + + def search + @params[:search].presence + end + + def sort + @params[:sort].presence || 'name' + end + + def filter_by_name(branches) + if search + branches.select { |branch| branch.name.include?(search) } + else + branches + end + end +end diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index bfd23aa4e04..3fc85dc6b2b 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -9,6 +9,17 @@ module BranchesHelper end end + def filter_branches_path(options = {}) + exist_opts = { + search: params[:search], + sort: params[:sort] + } + + options = exist_opts.merge(options) + + namespace_project_branches_path(@project.namespace, @project, @id, options) + end + def can_push_branch?(project, branch_name) return false unless project.repository.branch_exists?(branch_name) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index ffac28f78a0..ea2f5f9281a 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -45,10 +45,10 @@ module CiStatusHelper custom_icon(icon_name) end - def render_commit_status(commit, tooltip_placement: 'auto left', cssclass: '') + def render_commit_status(commit, tooltip_placement: 'auto left') project = commit.project path = builds_namespace_project_commit_path(project.namespace, project, commit) - render_status_with_link('commit', commit.status, path, tooltip_placement, cssclass: cssclass) + render_status_with_link('commit', commit.status, path, tooltip_placement) end def render_pipeline_status(pipeline, tooltip_placement: 'auto left') diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 052ce56809e..f497626e21a 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 module CommitsHelper # Returns a link to the commit author. If the author has a matching user and # is a member of the current @project it will link to the team member page. diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 75b029365f9..4c031942793 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -54,18 +54,20 @@ module DiffHelper end end - def organize_comments(left, right) - notes_left = notes_right = nil + def parallel_diff_discussions(left, right, diff_file) + discussion_left = discussion_right = nil - unless left[:type].nil? && right[:type] == 'new' - notes_left = @grouped_diff_notes[left[:line_code]] + if left && (left.unchanged? || left.removed?) + line_code = diff_file.line_code(left) + discussion_left = @grouped_diff_discussions[line_code] end - unless left[:type].nil? && right[:type].nil? - notes_right = @grouped_diff_notes[right[:line_code]] + if right && right.added? + line_code = diff_file.line_code(right) + discussion_right = @grouped_diff_discussions[line_code] end - [notes_left, notes_right] + [discussion_left, discussion_right] end def inline_diff_btn diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 98143dcee9b..0c47abe0fba 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -1,9 +1,4 @@ module NotesHelper - # Helps to distinguish e.g. commit notes in mr notes list - def note_for_main_target?(note) - @noteable.class.name == note.noteable_type && !note.diff_note? - end - def note_target_fields(note) if note.noteable hidden_field_tag(:target_type, note.noteable.class.name.underscore) + @@ -12,7 +7,7 @@ module NotesHelper end def note_editable?(note) - note.editable? && can?(current_user, :admin_note, note) + Ability.can_edit_note?(current_user, note) end def noteable_json(noteable) @@ -44,8 +39,8 @@ module NotesHelper # If we didn't, diff notes that would show for the same line on the changes # tab, would show in different discussions on the discussion tab. use_legacy_diff_note ||= begin - line_diff_notes = @grouped_diff_notes[line_code] - line_diff_notes && line_diff_notes.any?(&:legacy_diff_note?) + discussion = @grouped_diff_discussions[line_code] + discussion && discussion.legacy_diff_discussion? end data = { @@ -81,22 +76,10 @@ module NotesHelper data end - def link_to_reply_discussion(note, line_type = nil) + def link_to_reply_discussion(discussion, line_type = nil) return unless current_user - data = { - noteable_type: note.noteable_type, - noteable_id: note.noteable_id, - commit_id: note.commit_id, - discussion_id: note.discussion_id, - line_type: line_type - } - - if note.diff_note? - data[:note_type] = note.type - - data.merge!(note.diff_attributes) - end + data = discussion.reply_attributes.merge(line_type: line_type) content_tag(:div, class: "discussion-reply-holder") do button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button', @@ -104,23 +87,22 @@ module NotesHelper end end - def note_max_access_for_user(note) - @max_access_by_user_id ||= Hash.new do |hash, key| - project = key[:project] - hash[key] = project.team.human_max_access(key[:user_id]) - end + def preload_max_access_for_authors(notes, project) + user_ids = notes.map(&:author_id) + project.team.max_member_access_for_user_ids(user_ids) + end - full_key = { project: note.project, user_id: note.author_id } - @max_access_by_user_id[full_key] + def note_max_access_for_user(note) + note.project.team.human_max_access(note.author_id) end - def diff_note_path(note) - return unless note.diff_note? + def discussion_diff_path(discussion) + return unless discussion.diff_discussion? - if note.for_merge_request? && note.active? - diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) - elsif note.for_commit? - namespace_project_commit_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) + if discussion.for_merge_request? && discussion.active? + diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code) + elsif discussion.for_commit? + namespace_project_commit_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code) end end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index fcb2703e837..a2bba139c17 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -112,7 +112,8 @@ module SearchHelper search: params[:search], project_id: params[:project_id], group_id: params[:group_id], - scope: params[:scope] + scope: params[:scope], + repository_ref: params[:repository_ref] } options = exist_opts.merge(options) diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index bb395e37884..5f27e33c6ad 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -5,21 +5,9 @@ module SelectsHelper css_class << "skip_ldap " if opts[:skip_ldap] css_class << (opts[:class] || '') value = opts[:selected] || '' - - first_user = opts[:first_user] && current_user ? current_user.username : false - html = { class: css_class, - data: { - placeholder: opts[:placeholder] || 'Search for a user', - null_user: opts[:null_user] || false, - any_user: opts[:any_user] || false, - email_user: opts[:email_user] || false, - first_user: first_user, - current_user: opts[:current_user] || false, - "push-code-to-protected-branches" => opts[:push_code_to_protected_branches], - author_id: opts[:author_id] || '' - } + data: users_select_data_attributes(opts) } unless opts[:scope] == :all @@ -68,4 +56,20 @@ module SelectsHelper hidden_field_tag(id, value, class: css_class) end + + private + + def users_select_data_attributes(opts) + { + placeholder: opts[:placeholder] || 'Search for a user', + null_user: opts[:null_user] || false, + any_user: opts[:any_user] || false, + email_user: opts[:email_user] || false, + first_user: opts[:first_user] && current_user ? current_user.username : false, + current_user: opts[:current_user] || false, + "push-code-to-protected-branches" => opts[:push_code_to_protected_branches], + author_id: opts[:author_id] || '', + skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil, + } + end end diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb index 1926f03af07..790001222f1 100644 --- a/app/helpers/time_helper.rb +++ b/app/helpers/time_helper.rb @@ -1,5 +1,6 @@ module TimeHelper def time_interval_in_words(interval_in_seconds) + interval_in_seconds = interval_in_seconds.to_i minutes = interval_in_seconds / 60 seconds = interval_in_seconds - minutes * 60 diff --git a/app/models/ability.rb b/app/models/ability.rb index f33c8d61d3f..e47c5539f60 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -388,6 +388,18 @@ class Ability GroupProjectsFinder.new(group).execute(user).any? end + def can_edit_note?(user, note) + return false if !note.editable? || !user.present? + return true if note.author == user || user.admin? + + if note.project + max_access_level = note.project.team.max_member_access(user.id) + max_access_level >= Gitlab::Access::MASTER + else + false + end + end + def namespace_abilities(user, namespace) rules = [] diff --git a/app/models/blob.rb b/app/models/blob.rb index 4279ea2ce57..0df2805e448 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -31,6 +31,10 @@ class Blob < SimpleDelegator text? && language && language.name == 'SVG' end + def video? + UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.')) + end + def to_partial_path if lfs_pointer? 'download' diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 58f25b20acf..cbfa14e81f1 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -12,7 +12,7 @@ module Ci scope :unstarted, ->() { where(runner_id: nil) } scope :ignore_failures, ->() { where(allow_failure: false) } - scope :with_artifacts, ->() { where.not(artifacts_file: nil) } + scope :with_artifacts, ->() { where.not(artifacts_file: [nil, '']) } scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :manual_actions, ->() { where(when: :manual) } diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 6d3bbb03484..bce6a992af6 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -20,6 +20,11 @@ module Ci after_touch :update_state after_save :keep_around_commits + # ref can't be HEAD or SHA, can only be branch/tag name + scope :latest_successful_for, ->(ref = default_branch) do + where(ref: ref).success.order(id: :desc).limit(1) + end + def self.truncate_sha(sha) sha[0...8] end @@ -237,7 +242,7 @@ module Ci def keep_around_commits return unless project - + project.repository.keep_around(self.sha) project.repository.keep_around(self.before_sha) end diff --git a/app/models/commit.rb b/app/models/commit.rb index 2ef3973c160..f80f1063406 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -295,8 +295,8 @@ class Commit def uri_type(path) entry = @raw.tree.path(path) if entry[:type] == :blob - blob = Gitlab::Git::Blob.new(name: entry[:name]) - blob.image? ? :raw : :blob + blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name])) + blob.image? || blob.video? ? :raw : :blob else entry[:type] end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 535db26240a..2d185c28809 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -16,7 +16,11 @@ class CommitStatus < ActiveRecord::Base alias_attribute :author, :user - scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :commit_id)) } + scope :latest, -> do + max_id = unscope(:select).select("max(#{quoted_table_name}.id)") + + where(id: max_id.group(:name, :commit_id)) + end scope :retried, -> { where.not(id: latest) } scope :ordered, -> { order(:name) } scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb index 2785fbb21c9..4be6a2f621b 100644 --- a/app/models/concerns/note_on_diff.rb +++ b/app/models/concerns/note_on_diff.rb @@ -1,12 +1,6 @@ module NoteOnDiff extend ActiveSupport::Concern - NUMBER_OF_TRUNCATED_DIFF_LINES = 16 - - included do - delegate :blob, :highlighted_diff_lines, to: :diff_file, allow_nil: true - end - def diff_note? true end @@ -30,23 +24,4 @@ module NoteOnDiff def can_be_award_emoji? false end - - # Returns an array of at most 16 highlighted lines above a diff note - def truncated_diff_lines - prev_lines = [] - - highlighted_diff_lines.each do |line| - if line.meta? - prev_lines.clear - else - prev_lines << line - - break if for_line?(line) - - prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES - end - end - - prev_lines - end end diff --git a/app/models/discussion.rb b/app/models/discussion.rb new file mode 100644 index 00000000000..74facfd1c9c --- /dev/null +++ b/app/models/discussion.rb @@ -0,0 +1,91 @@ +class Discussion + NUMBER_OF_TRUNCATED_DIFF_LINES = 16 + + attr_reader :first_note, :notes + + delegate :created_at, + :project, + :author, + + :noteable, + :for_commit?, + :for_merge_request?, + + :line_code, + :diff_file, + :for_line?, + :active?, + + to: :first_note + + delegate :blob, :highlighted_diff_lines, to: :diff_file, allow_nil: true + + def self.for_notes(notes) + notes.group_by(&:discussion_id).values.map { |notes| new(notes) } + end + + def self.for_diff_notes(notes) + notes.group_by(&:line_code).values.map { |notes| new(notes) } + end + + def initialize(notes) + @first_note = notes.first + @notes = notes + end + + def id + first_note.discussion_id + end + + def diff_discussion? + first_note.diff_note? + end + + def legacy_diff_discussion? + notes.any?(&:legacy_diff_note?) + end + + def for_target?(target) + self.noteable == target && !diff_discussion? + end + + def expanded? + !diff_discussion? || active? + end + + def reply_attributes + data = { + noteable_type: first_note.noteable_type, + noteable_id: first_note.noteable_id, + commit_id: first_note.commit_id, + discussion_id: self.id, + } + + if diff_discussion? + data[:note_type] = first_note.type + + data.merge!(first_note.diff_attributes) + end + + data + end + + # Returns an array of at most 16 highlighted lines above a diff note + def truncated_diff_lines + prev_lines = [] + + highlighted_diff_lines.each do |line| + if line.meta? + prev_lines.clear + else + prev_lines << line + + break if for_line?(line) + + prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES + end + end + + prev_lines + end +end diff --git a/app/models/member.rb b/app/models/member.rb index 44db3d977fa..24ab1276ee9 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -53,6 +53,10 @@ class Member < ActiveRecord::Base default_value_for :notification_level, NotificationSetting.levels[:global] class << self + def access_for_user_ids(user_ids) + where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h + end + def find_by_invite_token(invite_token) invite_token = Devise.token_generator.digest(self, :invite_token, invite_token) find_by(invite_token: invite_token) diff --git a/app/models/note.rb b/app/models/note.rb index 0ce10c77de9..b6b2ac6aa42 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -69,7 +69,7 @@ class Note < ActiveRecord::Base project: [:project_members, { group: [:group_members] }]) end - before_validation :clear_blank_line_code! + before_validation :nullify_blank_type, :nullify_blank_line_code after_save :keep_around_commit class << self @@ -82,11 +82,12 @@ class Note < ActiveRecord::Base end def discussions - all.group_by(&:discussion_id).values + Discussion.for_notes(all) end - def grouped_diff_notes - diff_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code) + def grouped_diff_discussions + notes = diff_notes.fresh.select(&:active?) + Discussion.for_diff_notes(notes).map { |d| [d.line_code, d] }.to_h end # Searches for notes matching the given query. @@ -216,10 +217,6 @@ class Note < ActiveRecord::Base !system? end - def clear_blank_line_code! - self.line_code = nil if self.line_code.blank? - end - def can_be_award_emoji? noteable.is_a?(Awardable) end @@ -237,4 +234,12 @@ class Note < ActiveRecord::Base def keep_around_commit project.repository.keep_around(self.commit_id) end + + def nullify_blank_type + self.type = nil if self.type.blank? + end + + def nullify_blank_line_code + self.line_code = nil if self.line_code.blank? + end end diff --git a/app/models/project.rb b/app/models/project.rb index d6191e80e62..1616175709f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -429,6 +429,17 @@ class Project < ActiveRecord::Base repository.commit(ref) end + # ref can't be HEAD, can only be branch/tag name or SHA + def latest_successful_builds_for(ref = default_branch) + latest_pipeline = pipelines.latest_successful_for(ref).first + + if latest_pipeline + latest_pipeline.builds.latest.with_artifacts + else + builds.none + end + end + def merge_base_commit(first_commit_id, second_commit_id) sha = repository.merge_base(first_commit_id, second_commit_id) repository.commit(sha) if sha @@ -440,7 +451,9 @@ class Project < ActiveRecord::Base def add_import_job if forked? - job_id = RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path) + job_id = RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path, + forked_from_project.path_with_namespace, + self.namespace.path) else job_id = RepositoryImportWorker.perform_async(self.id) end @@ -871,9 +884,13 @@ class Project < ActiveRecord::Base old_path_with_namespace = File.join(namespace_dir, path_was) new_path_with_namespace = File.join(namespace_dir, path) + Rails.logger.error "Attempting to rename #{old_path_with_namespace} -> #{new_path_with_namespace}" + expire_caches_before_rename(old_path_with_namespace) if has_container_registry_tags? + Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present" + # we currently doesn't support renaming repository if it contains tags in container registry raise Exception.new('Project cannot be renamed, because tags are present in its container registry') end @@ -892,17 +909,22 @@ class Project < ActiveRecord::Base SystemHooksService.new.execute_hooks_for(self, :rename) @repository = nil - rescue + rescue => e + Rails.logger.error "Exception renaming #{old_path_with_namespace} -> #{new_path_with_namespace}: #{e}" # Returning false does not rollback after_* transaction but gives # us information about failing some of tasks false end else + Rails.logger.error "Repository could not be renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}" + # if we cannot move namespace directory we should rollback # db changes in order to prevent out of sync between db and fs raise Exception.new('repository cannot be renamed') end + Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}" + Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.path) end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 23e5b16221b..d7c986c1a91 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -46,7 +46,7 @@ class HipchatService < Service return unless supported_events.include?(data[:object_kind]) message = create_message(data) return unless message.present? - gate[room].send('GitLab', message, message_options) + gate[room].send('GitLab', message, message_options(data)) end def test(data) @@ -67,8 +67,8 @@ class HipchatService < Service @gate ||= HipChat::Client.new(token, options) end - def message_options - { notify: notify.present? && notify == '1', color: color || 'yellow' } + def message_options(data = nil) + { notify: notify.present? && notify == '1', color: message_color(data) } end def create_message(data) @@ -240,6 +240,21 @@ class HipchatService < Service "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)" end + def message_color(data) + build_status_color(data) || color || 'yellow' + end + + def build_status_color(data) + return unless data && data[:object_kind] == 'build' + + case data[:commit][:status] + when 'success' + 'green' + else + 'red' + end + end + def project_name project.name_with_namespace.gsub(/\s/, '') end diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 0b700930641..fdfaf052730 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -132,39 +132,63 @@ class ProjectTeam Gitlab::Access.options_with_owner.key(max_member_access(user_id)) end - # This method assumes project and group members are eager loaded for optimal - # performance. - def max_member_access(user_id) - access = [] + # Determine the maximum access level for a group of users in bulk. + # + # Returns a Hash mapping user ID -> maximum access level. + def max_member_access_for_user_ids(user_ids) + user_ids = user_ids.uniq + key = "max_member_access:#{project.id}" + RequestStore.store[key] ||= {} + access = RequestStore.store[key] - access += project.members.where(user_id: user_id).has_access.pluck(:access_level) + # Lookup only the IDs we need + user_ids = user_ids - access.keys - if group - access += group.members.where(user_id: user_id).has_access.pluck(:access_level) - end + if user_ids.present? + user_ids.each { |id| access[id] = Gitlab::Access::NO_ACCESS } - if project.invited_groups.any? && project.allowed_to_share_with_group? - access << max_invited_level(user_id) + member_access = project.members.access_for_user_ids(user_ids) + merge_max!(access, member_access) + + if group + group_access = group.members.access_for_user_ids(user_ids) + merge_max!(access, group_access) + end + + # Each group produces a list of maximum access level per user. We take the + # max of the values produced by each group. + if project.invited_groups.any? && project.allowed_to_share_with_group? + project.project_group_links.each do |group_link| + invited_access = max_invited_level_for_users(group_link, user_ids) + merge_max!(access, invited_access) + end + end end - access.compact.max + access + end + + def max_member_access(user_id) + max_member_access_for_user_ids([user_id])[user_id] end private - def max_invited_level(user_id) - project.project_group_links.map do |group_link| - invited_group = group_link.group - access = invited_group.group_members.find_by(user_id: user_id).try(:access_field) + # For a given group, return the maximum access level for the user. This is the min of + # the invited access level of the group and the access level of the user within the group. + # For example, if the group has been given DEVELOPER access but the member has MASTER access, + # the user should receive only DEVELOPER access. + def max_invited_level_for_users(group_link, user_ids) + invited_group = group_link.group + capped_access_level = group_link.group_access + access = invited_group.group_members.access_for_user_ids(user_ids) - # If group member has higher access level we should restrict it - # to max allowed access level - if access && access > group_link.group_access - access = group_link.group_access - end + # If the user is not in the list, assume he/she does not have access + missing_users = user_ids - access.keys + missing_users.each { |id| access[id] = Gitlab::Access::NO_ACCESS } - access - end.compact.max + # Cap the maximum access by the invited level access + access.each { |key, value| access[key] = [value, capped_access_level].min } end def fetch_members(level = nil) @@ -173,7 +197,7 @@ class ProjectTeam invited_members = [] if project.invited_groups.any? && project.allowed_to_share_with_group? - project.project_group_links.each do |group_link| + project.project_group_links.includes(group: [:group_members]).each do |group_link| invited_group = group_link.group im = invited_group.members @@ -215,4 +239,8 @@ class ProjectTeam def group project.group end + + def merge_max!(first_hash, second_hash) + first_hash.merge!(second_hash) { |_key, old, new| old > new ? old : new } + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 511df2d67c6..9fd8f5ec787 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -11,16 +11,6 @@ class Repository attr_accessor :path_with_namespace, :project - def self.clean_old_archives - Gitlab::Metrics.measure(:clean_old_archives) do - repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path - - return unless File.directory?(repository_downloads_path) - - Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete)) - end - end - def initialize(path_with_namespace, project) @path_with_namespace = path_with_namespace @project = project @@ -216,11 +206,23 @@ class Repository return if kept_around?(sha) - rugged.references.create(keep_around_ref_name(sha), sha) + # This will still fail if the file is corrupted (e.g. 0 bytes) + begin + rugged.references.create(keep_around_ref_name(sha), sha, force: true) + rescue Rugged::ReferenceError => ex + Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}" + rescue Rugged::OSError => ex + raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/ + Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}" + end end def kept_around?(sha) - ref_exists?(keep_around_ref_name(sha)) + begin + ref_exists?(keep_around_ref_name(sha)) + rescue Rugged::ReferenceError + false + end end def tag_names @@ -611,6 +613,8 @@ class Repository # Remove archives older than 2 hours def branches_sorted_by(value) case value + when 'name' + branches.sort_by(&:name) when 'recently_updated' branches.sort do |a, b| commit(b.target).committed_date <=> commit(a.target).committed_date @@ -984,6 +988,10 @@ class Repository if was_empty || !target_branch # Create branch rugged.references.create(ref, newrev) + + # If repo was empty expire cache + after_create if was_empty + after_create_branch else # Update head current_head = find_branch(branch).target @@ -1032,7 +1040,7 @@ class Repository private def cache - @cache ||= RepositoryCache.new(path_with_namespace) + @cache ||= RepositoryCache.new(path_with_namespace, @project.id) end def head_exists? diff --git a/app/models/service.rb b/app/models/service.rb index a8e1cc2f422..40cd9b861f0 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -26,7 +26,7 @@ class Service < ActiveRecord::Base scope :visible, -> { where.not(type: ['GitlabIssueTrackerService', 'GitlabCiService']) } scope :issue_trackers, -> { where(category: 'issue_tracker') } - scope :external_wikis, -> { where(type: 'ExternalWikiService') } + scope :external_wikis, -> { where(type: 'ExternalWikiService').active } scope :active, -> { where(active: true) } scope :without_defaults, -> { where(default: false) } diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 3d5fd9d3ee9..c3de278f5b7 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -44,7 +44,11 @@ class WikiPage # The escaped URL path of this page. def slug - @attributes[:slug] + if @attributes[:slug].present? + @attributes[:slug] + else + wiki.wiki.preview_page(title, '', format).url_path + end end alias_method :to_param, :slug diff --git a/app/services/repository_archive_clean_up_service.rb b/app/services/repository_archive_clean_up_service.rb new file mode 100644 index 00000000000..0b56b09738d --- /dev/null +++ b/app/services/repository_archive_clean_up_service.rb @@ -0,0 +1,33 @@ +class RepositoryArchiveCleanUpService + LAST_MODIFIED_TIME_IN_MINUTES = 120 + + def initialize(mmin = LAST_MODIFIED_TIME_IN_MINUTES) + @mmin = mmin + @path = Gitlab.config.gitlab.repository_downloads_path + end + + def execute + Gitlab::Metrics.measure(:repository_archive_clean_up) do + return unless File.directory?(path) + + clean_up_old_archives + clean_up_empty_directories + end + end + + private + + attr_reader :mmin, :path + + def clean_up_old_archives + run(%W(find #{path} -not -path #{path} -type f \( -name \*.tar -o -name \*.bz2 -o -name \*.tar.gz -o -name \*.zip \) -maxdepth 2 -mmin +#{mmin} -delete)) + end + + def clean_up_empty_directories + run(%W(find #{path} -not -path #{path} -type d -empty -name \*.git -maxdepth 1 -delete)) + end + + def run(cmd) + Gitlab::Popen.popen(cmd) + end +end diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb index 1cd93263c9f..b6c52ddac7a 100644 --- a/app/uploaders/artifact_uploader.rb +++ b/app/uploaders/artifact_uploader.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 class ArtifactUploader < CarrierWave::Uploader::Base storage :file diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index a65a896e41e..fb3b5dfecd0 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - class AttachmentUploader < CarrierWave::Uploader::Base include UploaderHelper diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb index 03d9329a14e..71ff14a3f20 100644 --- a/app/uploaders/avatar_uploader.rb +++ b/app/uploaders/avatar_uploader.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - class AvatarUploader < CarrierWave::Uploader::Base include UploaderHelper diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 1af9e9b0edb..3ac6030c21c 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 class FileUploader < CarrierWave::Uploader::Base include UploaderHelper MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)} @@ -33,16 +32,15 @@ class FileUploader < CarrierWave::Uploader::Base end def to_h - filename = image? ? self.file.basename : self.file.filename + filename = image_or_video? ? self.file.basename : self.file.filename escaped_filename = filename.gsub("]", "\\]") markdown = "[#{escaped_filename}](#{self.secure_url})" - markdown.prepend("!") if image? + markdown.prepend("!") if image_or_video? { alt: filename, url: self.secure_url, - is_image: image?, markdown: markdown } end diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb index 046a1d641a9..4f356dd663e 100644 --- a/app/uploaders/lfs_object_uploader.rb +++ b/app/uploaders/lfs_object_uploader.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - class LfsObjectUploader < CarrierWave::Uploader::Base storage :file diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb index 5ef440f3367..b10ad71d052 100644 --- a/app/uploaders/uploader_helper.rb +++ b/app/uploaders/uploader_helper.rb @@ -1,16 +1,37 @@ # Extra methods for uploader module UploaderHelper + IMAGE_EXT = %w[png jpg jpeg gif bmp tiff] + # We recommend using the .mp4 format over .mov. Videos in .mov format can + # still be used but you really need to make sure they are served with the + # proper MIME type video/mp4 and not video/quicktime or your videos won't play + # on IE >= 9. + # http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html + VIDEO_EXT = %w[mp4 m4v mov webm ogv] + def image? - img_ext = %w(png jpg jpeg gif bmp tiff) - if file.respond_to?(:extension) - img_ext.include?(file.extension.downcase) - else - # Not all CarrierWave storages respond to :extension - ext = file.path.split('.').last.downcase - img_ext.include?(ext) - end - rescue - false + extension_match?(IMAGE_EXT) + end + + def video? + extension_match?(VIDEO_EXT) + end + + def image_or_video? + image? || video? + end + + def extension_match?(extensions) + return false unless file + + extension = + if file.respond_to?(:extension) + file.extension + else + # Not all CarrierWave storages respond to :extension + File.extname(file.path).delete('.') + end + + extensions.include?(extension.downcase) end def file_storage? diff --git a/app/views/admin/background_jobs/_head.html.haml b/app/views/admin/background_jobs/_head.html.haml index 9d722bd7382..89d7a40d6b0 100644 --- a/app/views/admin/background_jobs/_head.html.haml +++ b/app/views/admin/background_jobs/_head.html.haml @@ -16,3 +16,7 @@ = link_to admin_health_check_path, title: 'Health Check' do %span Health Check + = nav_link(controller: :requests_profiles) do + = link_to admin_requests_profiles_path, title: 'Requests Profiles' do + %span + Requests Profiles diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index a2ac407c159..452fc25ab07 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -80,6 +80,10 @@ %span.pull-right = Gitlab::Shell.new.version %p + GitLab Workhorse + %span.pull-right + = Gitlab::Workhorse.version + %p GitLab API %span.pull-right = API::API::version diff --git a/app/views/admin/requests_profiles/index.html.haml b/app/views/admin/requests_profiles/index.html.haml new file mode 100644 index 00000000000..ae918086a57 --- /dev/null +++ b/app/views/admin/requests_profiles/index.html.haml @@ -0,0 +1,26 @@ +- @no_container = true +- page_title 'Requests Profiles' += render 'admin/background_jobs/head' + +%div{ class: container_class } + %h3.page-title + = page_title + + .bs-callout.clearfix + Pass the header + %code X-Profile-Token: #{@profile_token} + to profile the request + + - if @profiles.present? + .prepend-top-default + - @profiles.each do |path, profiles| + .panel.panel-default.panel-small + .panel-heading + %code= path + %ul.content-list + - profiles.each do |profile| + %li + = link_to profile.time.to_s(:long), admin_requests_profile_path(profile), data: {no_turbolink: true} + - else + %p + No profiles found diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml new file mode 100644 index 00000000000..fa1ad9efa73 --- /dev/null +++ b/app/views/discussions/_diff_discussion.html.haml @@ -0,0 +1,6 @@ +%tr.notes_holder + %td.notes_line{ colspan: 2 } + %td.notes_content + %ul.notes{ data: { discussion_id: discussion.id } } + = render partial: "projects/notes/note", collection: discussion.notes, as: :note + = link_to_reply_discussion(discussion) diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml new file mode 100644 index 00000000000..02b159ffd45 --- /dev/null +++ b/app/views/discussions/_diff_with_notes.html.haml @@ -0,0 +1,14 @@ +- diff_file = discussion.diff_file +- blob = discussion.blob + +.diff-file.file-holder + .file-title + = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: discussion.project, url: discussion_diff_path(discussion) + + .diff-content.code.js-syntax-highlight + %table + - discussion.truncated_diff_lines.each do |line| + = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true + + - if discussion.for_line?(line) + = render "discussions/diff_discussion", discussion: discussion diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml new file mode 100644 index 00000000000..49702e048aa --- /dev/null +++ b/app/views/discussions/_discussion.html.haml @@ -0,0 +1,45 @@ +- expanded = discussion.expanded? +%li.note.note-discussion.timeline-entry + .timeline-entry-inner + .timeline-icon + = link_to user_path(discussion.author) do + = image_tag avatar_icon(discussion.author), class: "avatar s40" + .timeline-content + .discussion.js-toggle-container{ class: discussion.id } + .discussion-header + = link_to_member(@project, discussion.author, avatar: false) + + .inline.discussion-headline-light + = discussion.author.to_reference + started a discussion on + + - if discussion.for_commit? + - commit = discussion.noteable + - if commit + commit + = link_to commit.short_id, namespace_project_commit_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code), class: 'monospace' + - else + a deleted commit + - else + - if discussion.active? + = link_to diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code) do + the diff + - else + an outdated diff + + = time_ago_with_tooltip(discussion.created_at, placement: "bottom", html_class: "note-created-ago") + + .discussion-actions + = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do + - if expanded + = icon("chevron-up") + - else + = icon("chevron-down") + + Toggle discussion + + .discussion-body.js-toggle-content{ class: ("hide" unless expanded) } + - if discussion.diff_discussion? && discussion.diff_file + = render "discussions/diff_with_notes", discussion: discussion + - else + = render "discussions/notes", discussion: discussion diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml new file mode 100644 index 00000000000..a2642b839f6 --- /dev/null +++ b/app/views/discussions/_notes.html.haml @@ -0,0 +1,5 @@ +.panel.panel-default + .notes{ data: { discussion_id: discussion.id } } + %ul.notes.timeline + = render partial: "projects/notes/note", collection: discussion.notes, as: :note + = link_to_reply_discussion(discussion) diff --git a/app/views/discussions/_parallel_diff_discussion.html.haml b/app/views/discussions/_parallel_diff_discussion.html.haml new file mode 100644 index 00000000000..a798c438ea0 --- /dev/null +++ b/app/views/discussions/_parallel_diff_discussion.html.haml @@ -0,0 +1,22 @@ +%tr.notes_holder + - if discussion_left + %td.notes_line.old + %td.notes_content.parallel.old + %ul.notes{ data: { discussion_id: discussion_left.id } } + = render partial: "projects/notes/note", collection: discussion_left.notes, as: :note + + = link_to_reply_discussion(discussion_left, 'old') + - else + %td.notes_line.old= "" + %td.notes_content.parallel.old= "" + + - if discussion_right + %td.notes_line.new + %td.notes_content.parallel.new + %ul.notes{ data: { discussion_id: discussion_right.id } } + = render partial: "projects/notes/note", collection: discussion_right.notes, as: :note + + = link_to_reply_discussion(discussion_right, 'new') + - else + %td.notes_line.new= "" + %td.notes_content.parallel.new= "" diff --git a/app/views/events/_event_scope.html.haml b/app/views/events/_event_scope.html.haml new file mode 100644 index 00000000000..8f7da7d8c4f --- /dev/null +++ b/app/views/events/_event_scope.html.haml @@ -0,0 +1,7 @@ +%span.event-scope + = event_preposition(event) + - if event.project + = link_to_project event.project + - else + = event.project_name + diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index 2e2403347c1..bba6e0d2c20 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -1,6 +1,6 @@ .event-title %span.author_name= link_to_author event - %span.event_label{class: event.action_name} + %span{class: event.action_name} - if event.target = event.action_name %strong @@ -10,12 +10,7 @@ - else = event_action_name(event) - = event_preposition(event) - - - if event.project - = link_to_project event.project - - else - = event.project_name + = render "events/event_scope", event: event - if event.target.respond_to?(:title) .event-body diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index 5a2a469ba62..aba64dd17d0 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -1,6 +1,6 @@ .event-title %span.author_name= link_to_author event - %span.event_label{class: event.action_name} + %span{class: event.action_name} = event_action_name(event) - if event.project diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml index 830fec0b4ab..f08c96df309 100644 --- a/app/views/events/event/_note.html.haml +++ b/app/views/events/event/_note.html.haml @@ -1,14 +1,9 @@ .event-title %span.author_name= link_to_author event - %span.event_label - = event.action_name - = event_note_title_html(event) - at + = event.action_name + = event_note_title_html(event) - - if event.project - = link_to_project event.project - - else - = event.project_name + = render "events/event_scope", event: event .event-body .event-note diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index ea54ef226ec..44fff49d99c 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -2,14 +2,14 @@ .event-title %span.author_name= link_to_author event - %span.event_label.pushed #{event.action_name} #{event.ref_type} + %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) - at - = link_to_project project + + = render "events/event_scope", event: event - if event.push_with_commits? .event-body diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index eddeae98bc4..53ed4fa991d 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -6,7 +6,7 @@ .cover-block.groups-cover-block %div{ class: container_class } - = image_tag group_icon(@group), class: "avatar group-avatar s70" + = image_tag group_icon(@group), class: "avatar group-avatar s70 avatar-tile" .group-info .cover-title %h1 diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 6e993e58f0d..15dd98077c8 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -74,6 +74,4 @@ = link_to "import flow", status_import_bitbucket_path, "data-no-turbolink" => "true" again. - -:javascript - new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}"); +.js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } } diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml index d3d3c595c17..c8a6fa1aa9e 100644 --- a/app/views/import/fogbugz/status.html.haml +++ b/app/views/import/fogbugz/status.html.haml @@ -56,5 +56,4 @@ Import = icon("spinner spin", class: "loading-icon") -:javascript - new ImporterStatus("#{jobs_import_fogbugz_path}", "#{import_fogbugz_path}"); +.js-importer-status{ data: { jobs_import_path: "#{jobs_import_fogbugz_path}", import_path: "#{import_fogbugz_path}" } } diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 7486b1423e2..deaaf9af875 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -55,5 +55,4 @@ Import = icon("spinner spin", class: "loading-icon") -:javascript - new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}"); +.js-importer-status{ data: { jobs_import_path: "#{jobs_import_github_path}", import_path: "#{import_github_path}" } } diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index aedb8468eca..fcfc6fd37f4 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -51,5 +51,4 @@ Import = icon("spinner spin", class: "loading-icon") -:javascript - new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}"); +.js-importer-status{ data: { jobs_import_path: "#{jobs_import_gitlab_path}", import_path: "#{import_gitlab_path}" } } diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml index 267eee4f262..ed3afb0ce33 100644 --- a/app/views/import/gitorious/status.html.haml +++ b/app/views/import/gitorious/status.html.haml @@ -51,5 +51,4 @@ Import = icon("spinner spin", class: "loading-icon") -:javascript - new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}"); +.js-importer-status{ data: { jobs_import_path: "#{jobs_import_gitorious_path}", import_path: "#{import_gitorious_path}" } } diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml index 5ada6b174eb..e79f122940a 100644 --- a/app/views/import/google_code/status.html.haml +++ b/app/views/import/google_code/status.html.haml @@ -77,5 +77,4 @@ = link_to "import flow", new_import_google_code_path again. -:javascript - new ImporterStatus("#{jobs_import_google_code_path}", "#{import_google_code_path}"); +.js-importer-status{ data: { jobs_import_path: "#{jobs_import_google_code_path}", import_path: "#{import_google_code_path}" } } diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 5ee8772882e..ac04f57e217 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -9,7 +9,7 @@ = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do %span Overview - = nav_link(controller: %w(system_info background_jobs logs health_check)) do + = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles)) do = link_to admin_system_info_path, title: 'Monitoring' do %span Monitoring diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 21668698814..3a14751ea8e 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -30,7 +30,7 @@ %span Merge Requests %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count) - = nav_link(controller: :snippets) do + = nav_link(controller: 'dashboard/snippets') do = link_to dashboard_snippets_path, title: 'Snippets' do %span Snippets diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 2049b204956..ee9c0366f2b 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -6,7 +6,7 @@ - content_for :scripts_body_top do - project = @target_project || @project - if @project_wiki && @page - - markdown_preview_path = namespace_project_wiki_markdown_preview_path(project.namespace, project, params[:id]) + - markdown_preview_path = namespace_project_wiki_markdown_preview_path(project.namespace, project, @page.slug) - else - markdown_preview_path = markdown_preview_namespace_project_path(project.namespace, project) - if current_user diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index 48b0dd6b121..ac50ce83f6a 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -5,7 +5,8 @@ %i.fa.fa-rss = render 'shared/event_filter' -.content_list{:"data-href" => activity_project_path(@project)} + +.content_list.project-activity{:"data-href" => activity_project_path(@project)} = spinner :javascript diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index cf11723dc8e..51f74f3b7ce 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,7 +1,7 @@ - empty_repo = @project.empty_repo? .project-home-panel.text-center{ class: ("empty-project" if empty_repo) } %div{ class: container_class } - = project_icon(@project, alt: @project.name, class: 'project-avatar avatar s70') + = project_icon(@project, alt: @project.name, class: 'project-avatar avatar s70 avatar-tile') %h1.project-title = @project.name %span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)} diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 77b405f1f39..cb190d121c2 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -7,28 +7,35 @@ .nav-text Protected branches can be managed in project settings - - if can? current_user, :push_code, @project - .nav-controls + .nav-controls + = form_tag(filter_branches_path, method: :get) do + = search_field_tag :search, params[:search], { placeholder: 'Filter by branch name', id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false } + + .dropdown.inline + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %span.light + - if params[:sort].present? + = params[:sort].humanize + - else + Name + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + %li + = link_to filter_branches_path(sort: nil) do + = sort_title_name + = link_to filter_branches_path(sort: 'recently_updated') do + = sort_title_recently_updated + = link_to filter_branches_path(sort: 'last_updated') do + = sort_title_oldest_updated + + - if can? current_user, :push_code, @project = link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do New branch - .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light - - if @sort.present? - = @sort.humanize - - else - Name - %b.caret - %ul.dropdown-menu.dropdown-menu-align-right - %li - = link_to namespace_project_branches_path(sort: nil) do - Name - = link_to namespace_project_branches_path(sort: 'recently_updated') do - = sort_title_recently_updated - = link_to namespace_project_branches_path(sort: 'last_updated') do - = sort_title_oldest_updated + - if @branches.any? %ul.content-list.all-branches - @branches.each do |branch| = render "projects/branches/branch", branch: branch = paginate @branches, theme: 'gitlab' + - else + .nothing-here-block No branches to show diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index dc57b49f27a..a8bc53c2849 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -40,7 +40,7 @@ .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) } .title Build details - - if @build.retryable? + - if can?(current_user, :update_build, @build) && @build.retryable? = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right', method: :post - if @build.merge_request %p.build-detail-row @@ -88,8 +88,9 @@ %p %span.build-light-text Variables: - %code - - @build.trigger_request.variables.each do |key, value| + + - @build.trigger_request.variables.each do |key, value| + %code #{key}=#{value} .block diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index a098a082854..d78888e9fe4 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -4,15 +4,11 @@ = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has-tooltip' do = custom_icon('icon_fork') Fork - %div.count-with-arrow - %span.arrow - %span.count - = @project.forks_count - else = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do = custom_icon('icon_fork') Fork - %div.count-with-arrow - %span.arrow - = link_to namespace_project_forks_path(@project.namespace, @project), class: "count" do - = @project.forks_count + %div.count-with-arrow + %span.arrow + = link_to namespace_project_forks_path(@project.namespace, @project), class: "count" do + = @project.forks_count diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 9264289987d..a3114771a42 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -14,16 +14,21 @@ %span ##{build.id} - if build.stuck? - = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') + .icon-container + = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') - if defined?(retried) && retried - = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') + .icon-container + = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') - if defined?(ref) && ref - if build.ref + .icon-container + = build.tag? ? icon('tag') : icon('code-fork') = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name" - else .light none - = custom_icon("icon_commit") + .icon-container + = custom_icon("icon_commit") - if defined?(commit_sha) && commit_sha = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace" @@ -88,4 +93,3 @@ - elsif build.playable? = link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do = icon('play') - diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 41fd5459429..540689f4a61 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -35,8 +35,8 @@ .bs-callout.bs-callout-warning \.gitlab-ci.yml not found in this commit -.table-holder - %table.table.builds +.table-holder.pipeline-holder + %table.table.builds.pipeline %thead %tr %th Status diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index ab9afb06afb..fd888f41b1e 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -19,13 +19,14 @@ · = commit.short_id - if commit.status - = render_commit_status(commit, cssclass: 'visible-xs-inline') + .visible-xs-inline + = render_commit_status(commit) - if commit.description? %a.text-expander.hidden-xs.js-toggle-button ... .commit-actions.hidden-xs - if commit.status - = render_commit_status(commit, cssclass: 'btn btn-transparent') + = render_commit_status(commit) = clipboard_button(clipboard_text: commit.id) = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" = link_to_browse_code(project, commit) diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index 65d68aa2985..f70dba224fa 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -17,6 +17,6 @@ - if local_assigns.fetch(:allow_rollback, false) = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do - if deployment.last? - Retry + Re-deploy - else Rollback diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 8ae433b4823..4bf3ccace20 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -7,7 +7,7 @@ .content-block.oneline-block.files-changed .inline-parallel-buttons - if !expand_all_diffs? && diff_files.any? { |diff_file| diff_file.collapsed? } - = link_to 'Expand all', url_for(params.merge(expand_all_diffs: 1, format: 'html')), class: 'btn btn-default' + = link_to 'Expand all', url_for(params.merge(expand_all_diffs: 1, format: nil)), class: 'btn btn-default' - if show_whitespace_toggle - if current_controller?(:commit) = commit_diff_whitespace_link(@project, @commit, class: 'hidden-xs') diff --git a/app/views/projects/diffs/_match_line_parallel.html.haml b/app/views/projects/diffs/_match_line_parallel.html.haml deleted file mode 100644 index b9c0d9dcdfd..00000000000 --- a/app/views/projects/diffs/_match_line_parallel.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -%td.old_line.diff-line-num.empty-cell -%td.line_content.parallel.match= line -%td.new_line.diff-line-num.empty-cell -%td.line_content.parallel.match= line diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index d208fcee10b..7f30faa20d8 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -5,32 +5,35 @@ - left = line[:left] - right = line[:right] %tr.line_holder.parallel - - if left[:type] == 'match' - = render "projects/diffs/match_line_parallel", { line: left[:text] } - - elsif left[:type] == 'nonewline' - %td.old_line.diff-line-num.empty-cell - %td.line_content.parallel.match= left[:text] - %td.new_line.diff-line-num.empty-cell - %td.line_content.parallel.match= left[:text] + - if left + - if left.meta? + %td.old_line.diff-line-num.empty-cell + %td.line_content.parallel.match= left.text + - else + - left_line_code = diff_file.line_code(left) + - left_position = diff_file.position(left) + %td.old_line.diff-line-num{id: left_line_code, class: left.type, data: { linenumber: left.old_pos }} + %a{href: "##{left_line_code}" }= raw(left.old_pos) + %td.line_content.parallel.noteable_line{class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old')}= diff_line_content(left.text) - else - %td.old_line.diff-line-num{id: left[:line_code], class: [left[:type], ('empty-cell' unless left[:number])], data: { linenumber: left[:number] }} - %a{href: "##{left[:line_code]}" }= raw(left[:number]) - %td.line_content.parallel.noteable_line{class: [left[:type], ('empty-cell' if left[:text].empty?)], data: diff_view_line_data(left[:line_code], left[:position], 'old')}= diff_line_content(left[:text]) + %td.old_line.diff-line-num.empty-cell + %td.line_content.parallel - - if right[:type] == 'new' - - new_line_type = 'new' - - new_line_code = right[:line_code] - - new_position = right[:position] + - if right + - if right.meta? + %td.old_line.diff-line-num.empty-cell + %td.line_content.parallel.match= left.text - else - - new_line_type = nil - - new_line_code = left[:line_code] - - new_position = left[:position] - - %td.new_line.diff-line-num{id: new_line_code, class: [new_line_type, ('empty-cell' unless right[:number])], data: { linenumber: right[:number] }} - %a{href: "##{new_line_code}" }= raw(right[:number]) - %td.line_content.parallel.noteable_line{class: [new_line_type, ('empty-cell' if right[:text].empty?)], data: diff_view_line_data(new_line_code, new_position, 'new')}= diff_line_content(right[:text]) + - right_line_code = diff_file.line_code(right) + - right_position = diff_file.position(right) + %td.new_line.diff-line-num{id: right_line_code, class: right.type, data: { linenumber: right.new_pos }} + %a{href: "##{right_line_code}" }= raw(right.new_pos) + %td.line_content.parallel.noteable_line{class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new')}= diff_line_content(right.text) + - else + %td.old_line.diff-line-num.empty-cell + %td.line_content.parallel - unless @diff_notes_disabled - - notes_left, notes_right = organize_comments(left, right) - - if notes_left.present? || notes_right.present? - = render "projects/notes/diff_notes_with_reply_parallel", notes_left: notes_left, notes_right: notes_right + - 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 diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 196f8122db3..5970b9abf2b 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -11,9 +11,9 @@ - unless @diff_notes_disabled - line_code = diff_file.line_code(line) - - diff_notes = @grouped_diff_notes[line_code] if line_code - - if diff_notes - = render "projects/notes/diff_notes_with_reply", notes: diff_notes + - discussion = @grouped_diff_discussions[line_code] if line_code + - if discussion + = render "discussions/diff_discussion", discussion: discussion - if last_line > 0 = render "projects/diffs/match_line", { line: "", diff --git a/app/views/projects/graphs/ci/_build_times.haml b/app/views/projects/graphs/ci/_build_times.haml index c58223fd39e..195f18afc76 100644 --- a/app/views/projects/graphs/ci/_build_times.haml +++ b/app/views/projects/graphs/ci/_build_times.haml @@ -19,4 +19,9 @@ ] } var ctx = $("#build_timesChart").get(0).getContext("2d"); - new Chart(ctx).Bar(data,{"scaleOverlay": true, responsive: true, maintainAspectRatio: false}); + var options = { scaleOverlay: true, responsive: true, maintainAspectRatio: false }; + if (window.innerWidth < 768) { + // Scale fonts if window width lower than 768px (iPad portrait) + options.scaleFontSize = 8 + } + new Chart(ctx).Bar(data, options); diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/graphs/ci/_builds.haml index 8fca07114fa..1fbf6ca2c1c 100644 --- a/app/views/projects/graphs/ci/_builds.haml +++ b/app/views/projects/graphs/ci/_builds.haml @@ -48,4 +48,9 @@ ] } var ctx = $("##{scope}Chart").get(0).getContext("2d"); - new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true, maintainAspectRatio: false}); + var options = { scaleOverlay: true, responsive: true, maintainAspectRatio: false }; + if (window.innerWidth < 768) { + // Scale fonts if window width lower than 768px (iPad portrait) + options.scaleFontSize = 8 + } + new Chart(ctx).Line(data, options); diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index 65db8af494d..7e34a89f9ae 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -59,6 +59,10 @@ var container = $(selector).parent(); var generateChart = function() { selector.attr('width', $(container).width()); + if (window.innerWidth < 768) { + // Scale fonts if window width lower than 768px (iPad portrait) + options.scaleFontSize = 8 + } return new Chart(ctx).Bar(data, options); }; // enabling auto-resizing diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index e93b7e0d66d..24749699c6d 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -2,7 +2,7 @@ .pull-right #new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)} = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), - method: :post, class: 'btn has-tooltip', title: @issue.to_branch_name, disabled: 'disabled' do + method: :post, class: 'btn btn-new btn-inverted has-tooltip', title: @issue.to_branch_name, disabled: 'disabled' do .checking = icon('spinner spin') Checking branches diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index c72d0140bb9..facdfcc9447 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -89,9 +89,9 @@ = link_to "#", class: 'btn js-toggle-button import_git' do %i.fa.fa-git %span Repo by URL - %div + %div{ class: 'import_gitlab_project' } - if gitlab_project_import_enabled? - = link_to new_import_gitlab_project_path, class: 'btn import_gitlab_project project-submit' do + = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do %i.fa.fa-gitlab %span GitLab export @@ -130,29 +130,29 @@ $(".modal").hide(); }); - $('.import_gitlab_project').bind('click', function() { - var _href = $("a.import_gitlab_project").attr("href"); - $(".import_gitlab_project").attr("href", _href + '?namespace_id=' + $("#project_namespace_id").val() + '&path=' + $("#project_path").val()); + $('.btn_import_gitlab_project').bind('click', function() { + var _href = $("a.btn_import_gitlab_project").attr("href"); + $(".btn_import_gitlab_project").attr("href", _href + '?namespace_id=' + $("#project_namespace_id").val() + '&path=' + $("#project_path").val()); }); - $('.import_gitlab_project').attr('disabled',true) - $('.import_gitlab_project').attr('title', 'Project path required.'); + $('.btn_import_gitlab_project').attr('disabled',true) + $('.import_gitlab_project').attr('title', 'Project path and name required.'); $('.import_gitlab_project').click(function( event ) { - if($('.import_gitlab_project').attr('disabled')) { + if($('.btn_import_gitlab_project').attr('disabled')) { event.preventDefault(); - new Flash("Please enter a path for the project to be imported to."); + new Flash("Please enter path and name for the project to be imported to."); } }); $('#project_path').keyup(function(){ if($(this).val().length !=0) { - $('.import_gitlab_project').attr('disabled', false); + $('.btn_import_gitlab_project').attr('disabled', false); $('.import_gitlab_project').attr('title',''); $(".flash-container").html("") } else { - $('.import_gitlab_project').attr('disabled',true); - $('.import_gitlab_project').attr('title', 'Project path required.'); + $('.btn_import_gitlab_project').attr('disabled',true); + $('.import_gitlab_project').attr('title', 'Project path and name required.'); } }); diff --git a/app/views/projects/notes/_diff_notes_with_reply.html.haml b/app/views/projects/notes/_diff_notes_with_reply.html.haml deleted file mode 100644 index ec6c4938efc..00000000000 --- a/app/views/projects/notes/_diff_notes_with_reply.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -- note = notes.first -%tr.notes_holder - %td.notes_line{ colspan: 2 } - %td.notes_content - %ul.notes{ data: { discussion_id: note.discussion_id } } - = render partial: "projects/notes/note", collection: notes, as: :note - = link_to_reply_discussion(note) diff --git a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml deleted file mode 100644 index e50a4f86d03..00000000000 --- a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml +++ /dev/null @@ -1,25 +0,0 @@ -- note_left = notes_left.present? ? notes_left.first : nil -- note_right = notes_right.present? ? notes_right.first : nil - -%tr.notes_holder - - if note_left - %td.notes_line.old - %td.notes_content.parallel.old - %ul.notes{ data: { discussion_id: note_left.discussion_id } } - = render partial: "projects/notes/note", collection: notes_left, as: :note - - = link_to_reply_discussion(note_left, 'old') - - else - %td.notes_line.old= "" - %td.notes_content.parallel.old= "" - - - if note_right - %td.notes_line.new - %td.notes_content.parallel.new - %ul.notes{ data: { discussion_id: note_right.discussion_id } } - = render partial: "projects/notes/note", collection: notes_right, as: :note - - = link_to_reply_discussion(note_right, 'new') - - else - %td.notes_line.new= "" - %td.notes_content.parallel.new= "" diff --git a/app/views/projects/notes/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml deleted file mode 100644 index 7869d6413d8..00000000000 --- a/app/views/projects/notes/_discussion.html.haml +++ /dev/null @@ -1,46 +0,0 @@ -- note = discussion_notes.first -- expanded = !note.diff_note? || note.active? -%li.note.note-discussion.timeline-entry - .timeline-entry-inner - .timeline-icon - = link_to user_path(note.author) do - = image_tag avatar_icon(note.author), class: "avatar s40" - .timeline-content - .discussion.js-toggle-container{ class: note.discussion_id } - .discussion-header - = link_to_member(@project, note.author, avatar: false) - - .inline.discussion-headline-light - = note.author.to_reference - started a discussion on - - - if note.for_commit? - - commit = note.noteable - - if commit - commit - = link_to commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code), class: 'monospace' - - else - a deleted commit - - else - - if note.active? - = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do - the diff - - else - an outdated diff - - = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "note-created-ago") - - .discussion-actions - = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do - - if expanded - = icon("chevron-up") - - else - = icon("chevron-down") - - Toggle discussion - - .discussion-body.js-toggle-content{ class: ("hide" unless expanded) } - - if note.diff_note? - = render "projects/notes/discussions/diff_with_notes", discussion_notes: discussion_notes - - else - = render "projects/notes/discussions/notes", discussion_notes: discussion_notes diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index af0046886fb..71da8ac9d7c 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -30,7 +30,7 @@ = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do = icon('trash-o') .note-body{class: note_editable ? 'js-task-list-container' : ''} - .note-text + .note-text.md = preserve do = note.note_html = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) diff --git a/app/views/projects/notes/_notes.html.haml b/app/views/projects/notes/_notes.html.haml index ebf7e8a9cb3..022578bd6db 100644 --- a/app/views/projects/notes/_notes.html.haml +++ b/app/views/projects/notes/_notes.html.haml @@ -1,10 +1,8 @@ - if @discussions.present? - - @discussions.each do |discussion_notes| - - note = discussion_notes.first - - if note_for_main_target?(note) - = render partial: "projects/notes/note", object: note, as: :note + - @discussions.each do |discussion| + - if discussion.for_target?(@noteable) + = render partial: "projects/notes/note", object: discussion.first_note, as: :note - else - = render 'projects/notes/discussion', discussion_notes: discussion_notes + = render 'discussions/discussion', discussion: discussion - else - - @notes.each do |note| - = render partial: "projects/notes/note", object: note, as: :note + = render partial: "projects/notes/note", collection: @notes, as: :note diff --git a/app/views/projects/notes/discussions/_diff_with_notes.html.haml b/app/views/projects/notes/discussions/_diff_with_notes.html.haml deleted file mode 100644 index 4a69b8f8840..00000000000 --- a/app/views/projects/notes/discussions/_diff_with_notes.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -- note = discussion_notes.first -- diff_file = note.diff_file -- return unless diff_file - -- blob = note.blob - -.diff-file.file-holder - .file-title - = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: note.project, url: diff_note_path(note) - - .diff-content.code.js-syntax-highlight - %table - - note.truncated_diff_lines.each do |line| - = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true - - - if note.for_line?(line) - = render "projects/notes/diff_notes_with_reply", notes: discussion_notes diff --git a/app/views/projects/notes/discussions/_notes.html.haml b/app/views/projects/notes/discussions/_notes.html.haml deleted file mode 100644 index a785149549d..00000000000 --- a/app/views/projects/notes/discussions/_notes.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -- note = discussion_notes.first -.panel.panel-default - .notes{ data: { discussion_id: note.discussion_id } } - %ul.notes.timeline - = render partial: "projects/notes/note", collection: discussion_notes, as: :note - = link_to_reply_discussion(note) diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 151e1d64851..950df740bbc 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -6,12 +6,13 @@ = page_title %p Keep stable branches secure and force developers to use merge requests. %p.prepend-top-20 - Protected branches are designed to: + By default, protected branches are designed to: %ul - %li prevent pushes from everybody except #{link_to "masters", help_page_path("user/permissions"), class: "vlink"} - %li prevent anyone from force pushing to the branch - %li prevent anyone from deleting the branch - %p.append-bottom-0 Read more about #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"} + %li prevent their creation, if not already created, from everybody except Masters + %li prevent pushes from everybody except Masters + %li prevent <strong>anyone</strong> from force pushing to the branch + %li prevent <strong>anyone</strong> from deleting the branch + %p.append-bottom-0 Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches"), class: "underlined-link"} and #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"}. .col-lg-9 %h5.prepend-top-0 Protect a branch @@ -23,7 +24,7 @@ = f.label :name, "Branch", class: "label-light" = render partial: "dropdown", locals: { f: f } %p.help-block - = link_to "Wildcards", help_page_path('workflow/protected_branches', anchor: "wildcard-protected-branches") + = link_to "Wildcards", help_page_path('user/project/protected_branches', anchor: "wildcard-protected-branches") such as %code *-stable or diff --git a/app/views/shared/icons/_icon_fork.svg b/app/views/shared/icons/_icon_fork.svg index 420ffe3a55b..a21f8f3a951 100644 --- a/app/views/shared/icons/_icon_fork.svg +++ b/app/views/shared/icons/_icon_fork.svg @@ -1 +1,3 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><circle id="a" cx="4" cy="4" r="4"/><mask id="d" width="8" height="8" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><circle id="b" cx="20" cy="4" r="4"/><mask id="e" width="8" height="8" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><circle id="c" cx="12" cy="30" r="4"/><mask id="f" width="8" height="8" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(8 3)"><path fill="#7E7E7E" d="M10 19.667c-4.14-1.29-7.389-5.878-7.389-5.878C2.274 13.353 2 12.545 2 12.01V6h4v5.509c0 .276.166.65.367.831 0 0 1.136 1.028 1.746 1.574C9.617 15.261 11.048 16 12.09 16c1.028 0 2.41-.723 3.858-2.048.588-.54 1.84-1.742 1.84-1.742a.784.784 0 0 0 .211-.502V6h4v6.008c0 .548-.259 1.349-.601 1.795 0 0-3.21 4.707-7.399 5.916V27h-4v-7.333z"/><use stroke="#7E7E7E" stroke-width="4" mask="url(#d)" xlink:href="#a"/><use stroke="#7E7E7E" stroke-width="4" mask="url(#e)" xlink:href="#b"/><use stroke="#7E7E7E" stroke-width="4" mask="url(#f)" xlink:href="#c"/></g></svg>
\ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"> + <path fill="#7E7E7E" fill-rule="evenodd" d="M22,29.5351288 L22,22.7193602 C26.1888699,21.5098039 29.3985457,16.802989 29.3985457,16.802989 C29.740988,16.3567547 30,15.5559546 30,15.0081969 L30,10.4648712 C31.1956027,9.77325238 32,8.48056471 32,7 C32,4.790861 30.209139,3 28,3 C25.790861,3 24,4.790861 24,7 C24,8.48056471 24.8043973,9.77325238 26,10.4648712 L26,14.7083871 C26,14.8784435 25.9055559,15.0987329 25.7890533,15.2104147 C25.7890533,15.2104147 24.5373893,16.4126202 23.9488702,16.9515733 C22.5015398,18.2770075 21.1191354,19 20.090554,19 C19.0477772,19 17.6172728,18.2608988 16.1128852,16.9142923 C15.5030182,16.3683886 14.3672121,15.3403307 14.3672121,15.3403307 C14.1659605,15.1583364 14.0000086,14.7846305 14.0000192,14.5088473 C14.0000192,14.5088473 14.0000932,12.7539451 14.0001308,10.4647956 C15.1956614,9.77315812 16,8.48051074 16,7 C16,4.790861 14.209139,3 12,3 C9.790861,3 8,4.790861 8,7 C8,8.48056471 8.80439726,9.77325238 10,10.4648712 L10,15.0081969 C10,15.5446944 10.2736352,16.3534183 10.6111812,16.7893819 C10.6111812,16.7893819 13.8599776,21.3779363 18,22.6668724 L18,29.5351288 C16.8043973,30.2267476 16,31.5194353 16,33 C16,35.209139 17.790861,37 20,37 C22.209139,37 24,35.209139 24,33 C24,31.5194353 23.1956027,30.2267476 22,29.5351288 Z M14,7 C14,5.8954305 13.1045695,5 12,5 C10.8954305,5 10,5.8954305 10,7 C10,8.1045695 10.8954305,9 12,9 C13.1045695,9 14,8.1045695 14,7 Z M30,7 C30,5.8954305 29.1045695,5 28,5 C26.8954305,5 26,5.8954305 26,7 C26,8.1045695 26.8954305,9 28,9 C29.1045695,9 30,8.1045695 30,7 Z M22,33 C22,31.8954305 21.1045695,31 20,31 C18.8954305,31 18,31.8954305 18,33 C18,34.1045695 18.8954305,35 20,35 C21.1045695,35 22,34.1045695 22,33 Z"/> +</svg> diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml index 0acb8253139..4e280c371ac 100644 --- a/app/views/shared/issuable/_label_page_default.html.haml +++ b/app/views/shared/issuable/_label_page_default.html.haml @@ -4,7 +4,7 @@ - filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels') .dropdown-page-one = dropdown_title(title) - = dropdown_filter(filter_placeholder, search_id: "label-name") + = dropdown_filter(filter_placeholder) = dropdown_content - if @project && show_footer = dropdown_footer do diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 8551288e2f2..0b6a01a3200 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -28,12 +28,12 @@ class EmailsOnPushWorker :push end - merge_base_sha = project.merge_base_commit(before_sha, after_sha).try(:sha) - diff_refs = nil compare = nil reverse_compare = false + if action == :push + merge_base_sha = project.merge_base_commit(before_sha, after_sha).try(:sha) compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha) diff_refs = Gitlab::Diff::DiffRefs.new( diff --git a/app/workers/repository_archive_cache_worker.rb b/app/workers/repository_archive_cache_worker.rb index 47c5a670ed4..a2e49c61f59 100644 --- a/app/workers/repository_archive_cache_worker.rb +++ b/app/workers/repository_archive_cache_worker.rb @@ -4,6 +4,6 @@ class RepositoryArchiveCacheWorker sidekiq_options queue: :default def perform - Repository.clean_old_archives + RepositoryArchiveCleanUpService.new.execute end end diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index f7604e48f83..d69d6037053 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -4,7 +4,7 @@ class RepositoryForkWorker sidekiq_options queue: :gitlab_shell - def perform(project_id, source_path, target_path) + def perform(project_id, forked_from_repository_storage_path, source_path, target_path) project = Project.find_by_id(project_id) unless project.present? @@ -12,7 +12,8 @@ class RepositoryForkWorker return end - result = gitlab_shell.fork_repository(project.repository_storage_path, source_path, target_path) + result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_path, + project.repository_storage_path, target_path) unless result logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}") project.mark_import_as_failed('The project could not be forked.') diff --git a/app/workers/requests_profiles_worker.rb b/app/workers/requests_profiles_worker.rb new file mode 100644 index 00000000000..9dd228a2483 --- /dev/null +++ b/app/workers/requests_profiles_worker.rb @@ -0,0 +1,9 @@ +class RequestsProfilesWorker + include Sidekiq::Worker + + sidekiq_options queue: :default + + def perform + Gitlab::RequestProfiler.remove_all_profiles + end +end diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml index 293f2b71d65..74325872b09 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -68,6 +68,25 @@ :why: https://opensource.org/licenses/BSD-2-Clause :versions: [] :when: 2016-05-02 05:55:09.796363000 Z +- - :whitelist + - LGPLv2+ + - :who: Stan Hu + :why: Equivalent to LGPLv2 + :versions: [] + :when: 2016-06-07 17:14:10.907682000 Z +- - :whitelist + - Artistic 2.0 + - :who: Josh Frye + :why: Disk/mount information display on Admin pages + :versions: [] + :when: 2016-06-29 16:32:45.432113000 Z +- - :whitelist + - Simplified BSD + - :who: Douwe Maan + :why: https://opensource.org/licenses/BSD-2-Clause + :versions: [] + :when: 2016-07-26 21:24:07.248480000 Z + # LICENSE BLACKLIST - - :blacklist @@ -175,15 +194,3 @@ :why: https://github.com/jmcnevin/rubypants/blob/master/LICENSE.rdoc :versions: [] :when: 2016-05-02 05:56:50.696858000 Z -- - :whitelist - - LGPLv2+ - - :who: Stan Hu - :why: Equivalent to LGPLv2 - :versions: [] - :when: 2016-06-07 17:14:10.907682000 Z -- - :whitelist - - Artistic 2.0 - - :who: Josh Frye - :why: Disk/mount information display on Admin pages - :versions: [] - :when: 2016-06-29 16:32:45.432113000 Z diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 325eca72862..1470a6e2550 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -106,8 +106,8 @@ production: &base ## Repository downloads directory # When a user clicks e.g. 'Download zip' on a project, a temporary zip file is created in the following directory. - # The default is 'tmp/repositories' relative to the root of the Rails app. - # repository_downloads_path: tmp/repositories + # The default is 'shared/cache/archive/' relative to the root of the Rails app. + # repository_downloads_path: shared/cache/archive/ ## Reply by email # Allow users to comment on issues and merge requests by replying to notification emails. diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 693507e0bec..49130f37b31 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -211,7 +211,6 @@ Settings.gitlab.default_projects_features['snippets'] = false if Setti Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil? Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil? Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) -Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil? Settings.gitlab['domain_whitelist'] ||= [] Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project] Settings.gitlab['trusted_proxies'] ||= [] @@ -291,6 +290,9 @@ Settings.cron_jobs['repository_archive_cache_worker']['job_class'] = 'Repository Settings.cron_jobs['gitlab_remove_project_export_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['gitlab_remove_project_export_worker']['cron'] ||= '0 * * * *' Settings.cron_jobs['gitlab_remove_project_export_worker']['job_class'] = 'GitlabRemoveProjectExportWorker' +Settings.cron_jobs['requests_profiles_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['requests_profiles_worker']['cron'] ||= '0 0 * * *' +Settings.cron_jobs['requests_profiles_worker']['job_class'] = 'RequestsProfilesWorker' # # GitLab Shell @@ -316,6 +318,21 @@ Settings.repositories['storages'] ||= {} Settings.repositories.storages['default'] ||= Settings.gitlab_shell['repos_path'] || Settings.gitlab['user_home'] + '/repositories/' # +# The repository_downloads_path is used to remove outdated repository +# archives, if someone has it configured incorrectly, and it points +# to the path where repositories are stored this can cause some +# data-integrity issue. In this case, we sets it to the default +# repository_downloads_path value. +# +repositories_storages_path = Settings.repositories.storages.values +repository_downloads_path = Settings.gitlab['repository_downloads_path'].to_s.gsub(/\/$/, '') +repository_downloads_full_path = File.expand_path(repository_downloads_path, Settings.gitlab['user_home']) + +if repository_downloads_path.blank? || repositories_storages_path.any? { |path| [repository_downloads_path, repository_downloads_full_path].include?(path.gsub(/\/$/, '')) } + Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') +end + +# # Backup # Settings['backup'] ||= Settingslogic.new({}) diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb index 3ba9e36c567..d92f64e1647 100644 --- a/config/initializers/6_validations.rb +++ b/config/initializers/6_validations.rb @@ -3,22 +3,27 @@ def storage_name_valid?(name) end def find_parent_path(name, path) + parent = Pathname.new(path).realpath.parent Gitlab.config.repositories.storages.detect do |n, p| - name != n && path.chomp('/').start_with?(p.chomp('/')) + name != n && Pathname.new(p).realpath == parent end end -def error(message) +def storage_validation_error(message) raise "#{message}. Please fix this in your gitlab.yml before starting GitLab." end -error('No repository storage path defined') if Gitlab.config.repositories.storages.empty? +def validate_storages + storage_validation_error('No repository storage path defined') if Gitlab.config.repositories.storages.empty? -Gitlab.config.repositories.storages.each do |name, path| - error("\"#{name}\" is not a valid storage name") unless storage_name_valid?(name) + Gitlab.config.repositories.storages.each do |name, path| + storage_validation_error("\"#{name}\" is not a valid storage name") unless storage_name_valid?(name) - parent_name, _parent_path = find_parent_path(name, path) - if parent_name - error("#{name} is a nested path of #{parent_name}. Nested paths are not supported for repository storages") + parent_name, _parent_path = find_parent_path(name, path) + if parent_name + storage_validation_error("#{name} is a nested path of #{parent_name}. Nested paths are not supported for repository storages") + end end end + +validate_storages unless Rails.env.test? || ENV['SKIP_STORAGE_VALIDATION'] == 'true' diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index c4266ab8ba5..f3cddac5b36 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -136,6 +136,13 @@ if Gitlab::Metrics.enabled? config.instrument_instance_methods(Rouge::Plugins::Redcarpet) config.instrument_instance_methods(Rouge::Formatters::HTMLGitlab) + [:XML, :HTML].each do |namespace| + namespace_mod = Nokogiri.const_get(namespace) + + config.instrument_methods(namespace_mod) + config.instrument_methods(namespace_mod::Document) + end + config.instrument_methods(Rinku) end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index ca58ae92d1b..3e553120205 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -6,5 +6,9 @@ Mime::Type.register_alias "text/plain", :diff Mime::Type.register_alias "text/plain", :patch -Mime::Type.register_alias 'text/html', :markdown -Mime::Type.register_alias 'text/html', :md +Mime::Type.register_alias "text/html", :markdown +Mime::Type.register_alias "text/html", :md + +Mime::Type.register "video/mp4", :mp4, [], [:m4v, :mov] +Mime::Type.register "video/webm", :webm +Mime::Type.register "video/ogg", :ogv diff --git a/config/initializers/request_profiler.rb b/config/initializers/request_profiler.rb new file mode 100644 index 00000000000..fb5a7b8372e --- /dev/null +++ b/config/initializers/request_profiler.rb @@ -0,0 +1,3 @@ +Rails.application.configure do |config| + config.middleware.use(Gitlab::RequestProfiler::Middleware) +end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 5e839327e7a..cf49ec2194c 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -7,6 +7,7 @@ Sidekiq.configure_server do |config| config.server_middleware do |chain| chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS'] chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] + chain.add Gitlab::SidekiqMiddleware::RequestStoreMiddleware unless ENV['SIDEKIQ_REQUEST_STORE'] == '0' end # Sidekiq-cron: load recurring jobs from gitlab.yml diff --git a/config/initializers/trusted_proxies.rb b/config/initializers/trusted_proxies.rb index df4a933e22f..30770b71e24 100644 --- a/config/initializers/trusted_proxies.rb +++ b/config/initializers/trusted_proxies.rb @@ -11,6 +11,12 @@ module Rack end end +gitlab_trusted_proxies = Array(Gitlab.config.gitlab.trusted_proxies).map do |proxy| + begin + IPAddr.new(proxy) + rescue IPAddr::InvalidAddressError + end +end.compact + Rails.application.config.action_dispatch.trusted_proxies = ( - [ '127.0.0.1', '::1' ] + Array(Gitlab.config.gitlab.trusted_proxies) -).map { |proxy| IPAddr.new(proxy) } + [ '127.0.0.1', '::1' ] + gitlab_trusted_proxies) diff --git a/config/routes.rb b/config/routes.rb index 21f3585bacd..414ba69dfae 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -42,10 +42,9 @@ Rails.application.routes.draw do resource :lint, only: [:show, :create] - resources :projects do + resources :projects, only: [:index, :show] do member do get :status, to: 'projects#badge' - get :integration end end @@ -144,13 +143,13 @@ Rails.application.routes.draw do get :jobs end - resource :gitlab, only: [:create, :new], controller: :gitlab do + resource :gitlab, only: [:create], controller: :gitlab do get :status get :callback get :jobs end - resource :bitbucket, only: [:create, :new], controller: :bitbucket do + resource :bitbucket, only: [:create], controller: :bitbucket do get :status get :callback get :jobs @@ -243,7 +242,6 @@ Rails.application.routes.draw do get :projects get :keys get :groups - put :team_update put :block put :unblock put :unlock @@ -281,6 +279,7 @@ Rails.application.routes.draw do resource :health_check, controller: 'health_check', only: [:show] resource :background_jobs, controller: 'background_jobs', only: [:show] resource :system_info, controller: 'system_info', only: [:show] + resources :requests_profiles, only: [:index, :show], param: :name resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do root to: 'projects#index', as: :projects @@ -300,7 +299,7 @@ Rails.application.routes.draw do end end - resource :appearances, path: 'appearance' do + resource :appearances, only: [:show, :create, :update], path: 'appearance' do member do get :preview delete :logo @@ -309,7 +308,7 @@ Rails.application.routes.draw do end resource :application_settings, only: [:show, :update] do - resources :services + resources :services, only: [:index, :edit, :update] put :reset_runners_token put :reset_health_check_token put :clear_repository_check_states @@ -346,7 +345,7 @@ Rails.application.routes.draw do end scope module: :profiles do - resource :account, only: [:show, :update] do + resource :account, only: [:show] do member do delete :unlink end @@ -358,7 +357,7 @@ Rails.application.routes.draw do end end resource :preferences, only: [:show, :update] - resources :keys + resources :keys, only: [:index, :show, :new, :create, :destroy] resources :emails, only: [:index, :create, :destroy] resource :avatar, only: [:destroy] @@ -660,7 +659,7 @@ Rails.application.routes.draw do post '/wikis/*id/markdown_preview', to: 'wikis#markdown_preview', constraints: WIKI_SLUG_ID, as: 'wiki_markdown_preview' end - resource :repository, only: [:show, :create] do + resource :repository, only: [:create] do member do get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex } end @@ -782,7 +781,7 @@ Rails.application.routes.draw do end end - resources :labels, constraints: { id: /\d+/ } do + resources :labels, except: [:show], constraints: { id: /\d+/ } do collection do post :generate post :set_priorities @@ -807,7 +806,7 @@ Rails.application.routes.draw do end end - resources :project_members, except: [:new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do + resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do collection do delete :leave diff --git a/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb b/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb new file mode 100644 index 00000000000..1eb99feb40c --- /dev/null +++ b/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb @@ -0,0 +1,15 @@ +class DropAndReaddHasExternalWikiInProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def up + update_column_in_batches(:projects, :has_external_wiki, nil) do |table, query| + query.where(table[:has_external_wiki].not_eq(nil)) + end + end + + def down + end +end diff --git a/db/migrate/20160722221922_nullify_blank_type_on_notes.rb b/db/migrate/20160722221922_nullify_blank_type_on_notes.rb new file mode 100644 index 00000000000..c4b78e8e15c --- /dev/null +++ b/db/migrate/20160722221922_nullify_blank_type_on_notes.rb @@ -0,0 +1,9 @@ +class NullifyBlankTypeOnNotes < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + execute "UPDATE notes SET type = NULL WHERE type = ''" + end +end diff --git a/db/schema.rb b/db/schema.rb index c7876426424..15cee55a7bf 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: 20160718153603) do +ActiveRecord::Schema.define(version: 20160722221922) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -84,7 +84,7 @@ ActiveRecord::Schema.define(version: 20160718153603) do t.string "health_check_access_token" t.boolean "send_user_confirmation_email", default: false t.integer "container_registry_token_expire_delay", default: 5 - t.boolean "user_default_external", default: false, null: false + t.boolean "user_default_external", default: false, null: false t.text "after_sign_up_text" t.string "repository_storage", default: "default" t.string "enabled_git_access_protocol" @@ -607,9 +607,9 @@ ActiveRecord::Schema.define(version: 20160718153603) do add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree create_table "merge_requests", force: :cascade do |t| - t.string "target_branch", null: false - t.string "source_branch", null: false - t.integer "source_project_id", null: false + t.string "target_branch", null: false + t.string "source_branch", null: false + t.integer "source_project_id", null: false t.integer "author_id" t.integer "assignee_id" t.string "title" @@ -618,20 +618,21 @@ ActiveRecord::Schema.define(version: 20160718153603) do t.integer "milestone_id" t.string "state" t.string "merge_status" - t.integer "target_project_id", null: false + t.integer "target_project_id", null: false t.integer "iid" t.text "description" - t.integer "position", default: 0 + t.integer "position", default: 0 t.datetime "locked_at" t.integer "updated_by_id" t.string "merge_error" t.text "merge_params" - t.boolean "merge_when_build_succeeds", default: false, null: false + t.boolean "merge_when_build_succeeds", default: false, null: false t.integer "merge_user_id" t.string "merge_commit_sha" t.datetime "deleted_at" t.string "in_progress_merge_commit_sha" end + add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree add_index "merge_requests", ["created_at", "id"], name: "index_merge_requests_on_created_at_and_id", using: :btree diff --git a/doc/README.md b/doc/README.md index cc0b6e0c1e5..b5b377822e6 100644 --- a/doc/README.md +++ b/doc/README.md @@ -21,7 +21,7 @@ ## Administrator documentation -- [Access restrictions](administration/access_restrictions.md) Define which Git access protocols can be used to talk to GitLab +- [Access restrictions](user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols) Define which Git access protocols can be used to talk to GitLab - [Authentication/Authorization](administration/auth/README.md) Configure external authentication with LDAP, SAML, CAS and additional Omniauth providers. - [Custom Git hooks](administration/custom_hooks.md) Custom Git hooks (on the filesystem) for when webhooks aren't enough. @@ -50,6 +50,7 @@ - [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs. - [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability. - [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab. +- [Multiple mountpoints for the repositories storage](administration/repository_storages.md) Define multiple repository storage paths to distribute the storage load. ## Contributor documentation diff --git a/doc/administration/img/access_restrictions.png b/doc/administration/img/access_restrictions.png Binary files differdeleted file mode 100644 index 66fd9491e85..00000000000 --- a/doc/administration/img/access_restrictions.png +++ /dev/null diff --git a/doc/administration/img/domain_blacklist.png b/doc/administration/img/domain_blacklist.png Binary files differdeleted file mode 100644 index a7894e5f08d..00000000000 --- a/doc/administration/img/domain_blacklist.png +++ /dev/null diff --git a/doc/administration/img/repository_storages_admin_ui.png b/doc/administration/img/repository_storages_admin_ui.png Binary files differnew file mode 100644 index 00000000000..599350bc098 --- /dev/null +++ b/doc/administration/img/repository_storages_admin_ui.png diff --git a/doc/administration/img/restricted_url.png b/doc/administration/img/restricted_url.png Binary files differdeleted file mode 100644 index 0a677433dcf..00000000000 --- a/doc/administration/img/restricted_url.png +++ /dev/null diff --git a/doc/administration/repository_storages.md b/doc/administration/repository_storages.md index 81bfe173151..55b054fc1a4 100644 --- a/doc/administration/repository_storages.md +++ b/doc/administration/repository_storages.md @@ -1,18 +1,99 @@ # Repository storages -GitLab allows you to define repository storage paths to enable distribution of -storage load between several mount points. - -## For installations from source +> [Introduced][ce-4578] in GitLab 8.10. -Add your repository storage paths in your `gitlab.yml` under repositories -> storages, using key -> value pairs. +GitLab allows you to define multiple repository storage paths to distribute the +storage load between several mount points. >**Notes:** +> - You must have at least one storage path called `default`. -- In order for backups to work correctly the storage path must **not** be a +- The paths are defined in key-value pairs. The key is an arbitrary name you + can pick to name the file path. +- The target directories and any of its subpaths must not be a symlink. + +## Configure GitLab + +>**Warning:** +In order for [backups] to work correctly, the storage path must **not** be a mount point and the GitLab user should have correct permissions for the parent -directory of the path. +directory of the path. In Omnibus GitLab this is taken care of automatically, +but for source installations you should be extra careful. +> +The thing is that for compatibility reasons `gitlab.yml` has a different +structure than Omnibus. In `gitlab.yml` you indicate the path for the +repositories, for example `/home/git/repositories`, while in Omnibus you +indicate `git_data_dirs`, which for the example above would be `/home/git`. +Then, Omnibus will create a `repositories` directory under that path to use with +`gitlab.yml`. +> +This little detail matters because while restoring a backup, the current +contents of `/home/git/repositories` [are moved to][raketask] `/home/git/repositories.old`, +so if `/home/git/repositories` is the mount point, then `mv` would be moving +things between mount points, and bad things could happen. Ideally, +`/home/git` would be the mount point, so then things would be moving within the +same mount point. This is guaranteed with Omnibus installations (because they +don't specify the full repository path but the parent path), but not for source +installations. + +--- + +Now that you've read that big fat warning above, let's edit the configuration +files and add the full paths of the alternative repository storage paths. In +the example below, we add two more mountpoints that are named `nfs` and `cephfs` +respectively. + +**For installations from source** + +1. Edit `gitlab.yml` and add the storage paths: + + ```yaml + repositories: + # Paths where repositories can be stored. Give the canonicalized absolute pathname. + # NOTE: REPOS PATHS MUST NOT CONTAIN ANY SYMLINK!!! + storages: # You must have at least a 'default' storage path. + default: /home/git/repositories + nfs: /mnt/nfs/repositories + cephfs: /mnt/cephfs/repositories + ``` + +1. [Restart GitLab] for the changes to take effect. + +>**Note:** +The [`gitlab_shell: repos_path` entry][repospath] in `gitlab.yml` will be +deprecated and replaced by `repositories: storages` in the future, so if you +are upgrading from a version prior to 8.10, make sure to add the configuration +as described in the step above. After you make the changes and confirm they are +working, you can remove the `repos_path` line. + +--- + +**For Omnibus installations** + +1. Edit `/etc/gitlab/gitlab.rb` by appending the rest of the paths to the + default one: + + ```ruby + git_data_dirs({ + "default" => "/var/opt/gitlab/git-data", + "nfs" => "/mnt/nfs/git-data", + "cephfs" => "/mnt/cephfs/git-data" + }) + ``` + + Note that Omnibus stores the repositories in a `repositories` subdirectory + of the `git-data` directory. + +## Choose where new project repositories will be stored + +Once you set the multiple storage paths, you can choose where new projects will +be stored via the **Application Settings** in the Admin area. -## For omnibus installations + -Follow the instructions at https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/configuration.md#storing-git-data-in-an-alternative-directory +[ce-4578]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4578 +[restart gitlab]: restart_gitlab.md#installations-from-source +[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure +[backups]: ../raketasks/backup_restore.md +[raketask]: https://gitlab.com/gitlab-org/gitlab-ce/blob/033e5423a2594e08a7ebcd2379bd2331f4c39032/lib/backup/repository.rb#L54-56 +[repospath]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-9-stable/config/gitlab.yml.example#L457 diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md index b44f8cfd628..796b3680a75 100644 --- a/doc/api/award_emoji.md +++ b/doc/api/award_emoji.md @@ -67,9 +67,9 @@ Example Response: ] ``` -### Get single issue note +### Get single award emoji -Gets a single award emoji +Gets a single award emoji from an issue or merge request. ``` GET /projects/:id/issues/:issue_id/award_emoji/:award_id diff --git a/doc/api/builds.md b/doc/api/builds.md index 2adea11247e..24d90e22a9b 100644 --- a/doc/api/builds.md +++ b/doc/api/builds.md @@ -283,6 +283,40 @@ Response: [ce-2893]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2893 +## Download the artifacts file + +> [Introduced][ce-5347] in GitLab 8.10. + +Download the artifacts file from the given reference name and job provided the +build finished successfully. + +``` +GET /projects/:id/builds/artifacts/:ref_name/download?job=name +``` + +Parameters + +| Attribute | Type | Required | Description | +|-------------|---------|----------|-------------------------- | +| `id` | integer | yes | The ID of a project | +| `ref_name` | string | yes | The ref from a repository | +| `job` | string | yes | The name of the job | + +Example request: + +``` +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/artifacts/master/download?job=test" +``` + +Example response: + +| Status | Description | +|-----------|---------------------------------| +| 200 | Serves the artifacts file | +| 404 | Build not found or no artifacts | + +[ce-5347]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5347 + ## Get a trace file Get a trace of a specific build of a project @@ -409,7 +443,7 @@ POST /projects/:id/builds/:build_id/erase Parameters -| Attribute | Type | required | Description | +| Attribute | Type | Required | Description | |-------------|---------|----------|---------------------| | `id` | integer | yes | The ID of a project | | `build_id` | integer | yes | The ID of a build | @@ -459,7 +493,7 @@ POST /projects/:id/builds/:build_id/artifacts/keep Parameters -| Attribute | Type | required | Description | +| Attribute | Type | Required | Description | |-------------|---------|----------|---------------------| | `id` | integer | yes | The ID of a project | | `build_id` | integer | yes | The ID of a build | diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index a8c3b068d22..e00882e6d5d 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -276,6 +276,7 @@ Parameters: ```json { "id": 1, + "iid": 1, "target_branch": "master", "source_branch": "test1", "project_id": 3, @@ -350,6 +351,7 @@ Parameters: ```json { "id": 1, + "iid": 1, "target_branch": "master", "project_id": 3, "title": "test1", @@ -449,6 +451,7 @@ Parameters: ```json { "id": 1, + "iid": 1, "target_branch": "master", "source_branch": "test1", "project_id": 3, @@ -517,6 +520,7 @@ Parameters: ```json { "id": 1, + "iid": 1, "target_branch": "master", "source_branch": "test1", "project_id": 3, diff --git a/doc/api/projects.md b/doc/api/projects.md index dceee7b4ea7..0ba0bffb4ac 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -850,7 +850,6 @@ Parameters: { "alt": "dk", "url": "/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png", - "is_image": true, "markdown": "" } ``` diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index 7f83f846454..0f64137a8a9 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -38,7 +38,7 @@ GitLab Runner then executes build scripts as the `gitlab-runner` user. $ sudo gitlab-ci-multi-runner register -n \ --url https://gitlab.com/ci \ --registration-token REGISTRATION_TOKEN \ - --executor shell + --executor shell \ --description "My Runner" ``` diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 4bf610f0e9a..4a7c21f811d 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -38,7 +38,7 @@ The `API_TOKEN` will take the Secure Variable value: `SECURE`. | **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally | | **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally | | **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built | -| **CI_PROJECT_NAMESPACE**| 8.10 | 0.5 | The project namespace that is currently being built | +| **CI_PROJECT_NAMESPACE**| 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built | | **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name | | **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project | | **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the build is run | diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 01b7748e90e..ea3fff1596e 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -13,34 +13,36 @@ If you want a quick introduction to GitLab CI, follow our **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - [.gitlab-ci.yml](#gitlab-ci-yml) - - [image and services](#image-and-services) - - [before_script](#before_script) - - [after_script](#after_script) - - [stages](#stages) - - [types](#types) - - [variables](#variables) - - [cache](#cache) - - [cache:key](#cache-key) + - [image and services](#image-and-services) + - [before_script](#before_script) + - [after_script](#after_script) + - [stages](#stages) + - [types](#types) + - [variables](#variables) + - [cache](#cache) + - [cache:key](#cache-key) - [Jobs](#jobs) - - [script](#script) - - [stage](#stage) - - [only and except](#only-and-except) - - [job variables](#job-variables) - - [tags](#tags) - - [when](#when) - - [environment](#environment) - - [artifacts](#artifacts) - - [artifacts:name](#artifactsname) - - [artifacts:when](#artifactswhen) - - [artifacts:expire_in](#artifactsexpire_in) - - [dependencies](#dependencies) - - [before_script and after_script](#before_script-and-after_script) + - [script](#script) + - [stage](#stage) + - [only and except](#only-and-except) + - [job variables](#job-variables) + - [tags](#tags) + - [allow_failure](#allow_failure) + - [when](#when) + - [Manual actions](#manual-actions) + - [environment](#environment) + - [artifacts](#artifacts) + - [artifacts:name](#artifacts-name) + - [artifacts:when](#artifacts-when) + - [artifacts:expire_in](#artifacts-expire_in) + - [dependencies](#dependencies) + - [before_script and after_script](#before_script-and-after_script) - [Git Strategy](#git-strategy) - [Shallow cloning](#shallow-cloning) - [Hidden jobs](#hidden-jobs) - [Special YAML features](#special-yaml-features) - - [Anchors](#anchors) -- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ciyml) + - [Anchors](#anchors) +- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ci-yml) - [Skipping builds](#skipping-builds) - [Examples](#examples) @@ -473,6 +475,39 @@ job: The specification above, will make sure that `job` is built by a Runner that has both `ruby` AND `postgres` tags defined. +### allow_failure + +`allow_failure` is used when you want to allow a build to fail without impacting +the rest of the CI suite. Failed builds don't contribute to the commit status. + +When enabled and the build fails, the pipeline will be successful/green for all +intents and purposes, but a "CI build passed with warnings" message will be +displayed on the merge request or commit or build page. This is to be used by +builds that are allowed to fail, but where failure indicates some other (manual) +steps should be taken elsewhere. + +In the example below, `job1` and `job2` will run in parallel, but if `job1` +fails, it will not stop the next stage from running, since it's marked with +`allow_failure: true`: + +```yaml +job1: + stage: test + script: + - execute_script_that_will_fail + allow_failure: true + +job2: + stage: test + script: + - execute_script_that_will_succeed + +job3: + stage: deploy + script: + - deploy_to_staging +``` + ### when `when` is used to implement jobs that are run in case of failure or despite the @@ -485,7 +520,8 @@ failure. 1. `on_failure` - execute build only when at least one build from prior stages fails. 1. `always` - execute build regardless of the status of builds from prior stages. -1. `manual` - execute build manually. +1. `manual` - execute build manually (added in GitLab 8.10). Read about + [manual actions](#manual-actions) below. For example: @@ -528,21 +564,22 @@ cleanup_job: The above script will: -1. Execute `cleanup_build_job` only when `build_job` fails -2. Always execute `cleanup_job` as the last step in pipeline -3. Allow you to manually execute `deploy_job` from GitLab +1. Execute `cleanup_build_job` only when `build_job` fails. +2. Always execute `cleanup_job` as the last step in pipeline regardless of + success or failure. +3. Allow you to manually execute `deploy_job` from GitLab's UI. #### Manual actions >**Note:** Introduced in GitLab 8.10. -Manual actions are special type of jobs that are not executed automatically in pipeline. -They need to be explicitly started by the user. -Manual actions can be started from pipelines, builds, environments and deployments views. -You can execute the same manual action multiple times. +Manual actions are a special type of job that are not executed automatically; +they need to be explicitly started by a user. Manual actions can be started +from pipeline, build, environment, and deployment views. You can execute the +same manual action multiple times. -Example usage of manual actions is deployment, ex. promote a staging environment to production. +An example usage of manual actions is deployment to production. ### environment diff --git a/doc/install/installation.md b/doc/install/installation.md index 19d083d580d..9bc0dbb5e2a 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -398,7 +398,7 @@ If you are not using Linux you may have to run `gmake` instead of cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git cd gitlab-workhorse - sudo -u git -H git checkout v0.7.7 + sudo -u git -H git checkout v0.7.8 sudo -u git -H make ### Initialize Database and Activate Advanced Features diff --git a/doc/integration/README.md b/doc/integration/README.md index fd330dd7a7d..ddbd570ac6c 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -11,7 +11,6 @@ See the documentation below for details on how to configure these services. - [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure - [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider - [CAS](cas.md) Configure GitLab to sign in using CAS -- [Slack](slack.md) Integrate with the Slack chat service - [OAuth2 provider](oauth_provider.md) OAuth2 application creation - [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages - [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users diff --git a/doc/integration/slack.md b/doc/integration/slack.md index 11f956fed3e..8cd151fbf95 100644 --- a/doc/integration/slack.md +++ b/doc/integration/slack.md @@ -1,40 +1 @@ -# Slack integration - -## On Slack - -To enable Slack integration you must create an Incoming WebHooks integration on Slack: - -1. [Sign in to Slack](https://slack.com/signin) - -1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/) - -1. Choose the channel name you want to send notifications to. - -1. Click **Add Incoming WebHooks Integration** - - Optional step; You can change bot's name and avatar by clicking modifying the bot name or avatar under **Integration Settings**. - -1. Copy the **Webhook URL**, we'll need this later for GitLab. - - -## On GitLab - -After Slack is ready we need to setup GitLab. Here are the steps to achieve this. - -1. Sign in to GitLab - -1. Pick the repository you want. - -1. Navigate to Settings -> Services -> Slack - -1. Pick the triggers you want to activate and respective channel (`#general` by default). - -1. Fill in your Slack details - - Webhook: Paste the Webhook URL from the step above - - Username: Fill this in if you want to change the username of the bot - - Mark it as active - -1. Save your settings - -Have fun :) - -*P.S. You can set "branch,pushed,Compare changes" as highlight words on your Slack profile settings, so that you can be aware of new commits when somebody pushes them.* +This document was moved to [project_services/slack.md](../project_services/slack.md). diff --git a/doc/markdown/img/video.mp4 b/doc/markdown/img/video.mp4 Binary files differnew file mode 100644 index 00000000000..1fc478842f5 --- /dev/null +++ b/doc/markdown/img/video.mp4 diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index fb2dd582754..c6c7ac81c0d 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -13,6 +13,7 @@ * [Emoji](#emoji) * [Special GitLab references](#special-gitlab-references) * [Task Lists](#task-lists) +* [Videos](#videos) **[Standard Markdown](#standard-markdown)** @@ -31,26 +32,39 @@ ## GitLab Flavored Markdown (GFM) +> **Note:** +Not all of the GitLab-specific extensions to Markdown that are described in +this document currently work on our documentation website. +> +For the best result, we encourage you to check this document out as rendered +by GitLab: [markdown.md] + _GitLab uses the [Redcarpet Ruby library][redcarpet] for Markdown processing._ GitLab uses "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/). -You can use GFM in +You can use GFM in the following areas: - comments - issues - merge requests - milestones +- snippets (the snippet must be named with a `.md` extension) - wiki pages +- markdown documents inside the repository -You can also use other rich text files in GitLab. You might have to install a dependency to do so. Please see the [github-markup gem readme](https://github.com/gitlabhq/markup#markups) for more information. +You can also use other rich text files in GitLab. You might have to install a +dependency to do so. Please see the [github-markup gem readme](https://github.com/gitlabhq/markup#markups) for more information. ## Newlines +> If this is not rendered correctly, see +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#newlines + GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p). A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines. -Line-breaks, or softreturns, are rendered if you end a line with two or more spaces +Line-breaks, or softreturns, are rendered if you end a line with two or more spaces: Roses are red [followed by two or more spaces] Violets are blue @@ -64,17 +78,25 @@ Sugar is sweet ## Multiple underscores in words -It is not reasonable to italicize just _part_ of a word, especially when you're dealing with code and names that often appear with multiple underscores. Therefore, GFM ignores multiple underscores in words. +> If this is not rendered correctly, see +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#multiple-underscores-in-words + +It is not reasonable to italicize just _part_ of a word, especially when you're dealing with code and names that often appear with multiple underscores. Therefore, GFM ignores multiple underscores in words: perform_complicated_task + do_this_and_do_that_and_another_thing perform_complicated_task + do_this_and_do_that_and_another_thing ## URL auto-linking -GFM will autolink almost any URL you copy and paste into your text. +> If this is not rendered correctly, see +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#url-auto-linking + +GFM will autolink almost any URL you copy and paste into your text: * https://www.google.com * https://google.com/ @@ -92,8 +114,11 @@ GFM will autolink almost any URL you copy and paste into your text. ## Multiline Blockquote +> If this is not rendered correctly, see +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#multiline-blockquote + On top of standard Markdown [blockquotes](#blockquotes), which require prepending `>` to quoted lines, -GFM supports multiline blockquotes fenced by <code>>>></code>. +GFM supports multiline blockquotes fenced by <code>>>></code>: ```no-highlight >>> @@ -123,10 +148,15 @@ you can quote that without having to manually prepend `>` to every line! ## Code and Syntax Highlighting +> If this is not rendered correctly, see +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#code-and-syntax-highlighting + _GitLab uses the [Rouge Ruby library][rouge] for syntax highlighting. For a list of supported languages visit the Rouge website._ -Blocks of code are either fenced by lines with three back-ticks <code>```</code>, or are indented with four spaces. Only the fenced code blocks support syntax highlighting. +Blocks of code are either fenced by lines with three back-ticks <code>```</code>, +or are indented with four spaces. Only the fenced code blocks support syntax +highlighting: ```no-highlight Inline `code` has `back-ticks around` it. @@ -188,6 +218,9 @@ But let's throw in a <b>tag</b>. ## Inline Diff +> If this is not rendered correctly, see +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#inline-diff + With inline diffs tags you can display {+ additions +} or [- deletions -]. The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}. @@ -201,6 +234,9 @@ However the wrapping tags cannot be mixed as such: ## Emoji +> If this is not rendered correctly, see +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#emoji + Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: :zap: You can use emoji anywhere GFM is supported. :v: @@ -263,6 +299,9 @@ GFM also recognizes certain cross-project references: ## Task Lists +> If this is not rendered correctly, see +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#task-lists + You can add task lists to issues, merge requests and comments. To create a task list, add a specially-formatted Markdown list, like so: ```no-highlight @@ -281,6 +320,23 @@ You can add task lists to issues, merge requests and comments. To create a task Task lists can only be created in descriptions, not in titles. Task item state can be managed by editing the description's Markdown or by toggling the rendered check boxes. +## Videos + +> If this is not rendered correctly, see +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#videos + +Image tags with a video extension are automatically converted to a video player. + +The valid video extensions are `.mp4`, `.m4v`, `.mov`, `.webm`, and `.ogv`. + + Here's a sample video: + +  + +Here's a sample video: + + + # Standard Markdown ## Headers @@ -584,7 +640,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa This line is also a separate paragraph, but... This line is only separated by a single newline, so it's a separate line in the *same paragraph*. -This line is also a separate paragraph, and... +This line is also a separate paragraph, and... This line is on its own line, because the previous line ends with two spaces. ``` @@ -596,7 +652,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa This line is also begins a separate paragraph, but... This line is only separated by a single newline, so it's a separate line in the *same paragraph*. -This line is also a separate paragraph, and... +This line is also a separate paragraph, and... This line is on its own line, because the previous line ends with two spaces. @@ -642,6 +698,7 @@ By including colons in the header row, you can align the text within that column - The [Markdown Syntax Guide](https://daringfireball.net/projects/markdown/syntax) at Daring Fireball is an excellent resource for a detailed explanation of standard markdown. - [Dillinger.io](http://dillinger.io) is a handy tool for testing standard markdown. +[markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md [rouge]: http://rouge.jneen.net/ "Rouge website" [redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website" [^1]: This link will be broken if you see this document from the Help page or docs.gitlab.com diff --git a/doc/project_services/img/slack_configuration.png b/doc/project_services/img/slack_configuration.png Binary files differindex d3ebe5969af..b8de8a56db7 100644 --- a/doc/project_services/img/slack_configuration.png +++ b/doc/project_services/img/slack_configuration.png diff --git a/doc/project_services/slack.md b/doc/project_services/slack.md index 4ed33838f7c..3cfe77c9f85 100644 --- a/doc/project_services/slack.md +++ b/doc/project_services/slack.md @@ -1,26 +1,50 @@ # Slack Service -Go to your project's **Settings > Services > Slack** and you will see a checkbox with the following events that can be triggered: +## On Slack -* Push -* Issue -* Merge request -* Note -* Tag push -* Build -* Wiki page +To enable Slack integration you must create an incoming webhook integration on +Slack: -Below each of these event checkboxes you will have an input to insert which Slack channel do you want to send that event message, -`#general` channel is default. +1. [Sign in to Slack](https://slack.com/signin) +1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/) +1. Choose the channel name you want to send notifications to. +1. Click **Add Incoming WebHooks Integration** +1. Copy the **Webhook URL**, we'll need this later for GitLab. +## On GitLab - +After you set up Slack, it's time to set up GitLab. + +Go to your project's **Settings > Services > Slack** and you will see a +checkbox with the following events that can be triggered: + +- Push +- Issue +- Merge request +- Note +- Tag push +- Build +- Wiki page + +Bellow each of these event checkboxes, you will have an input field to insert +which Slack channel you want to send that event message, with `#general` +being the default. Enter your preferred channel **without** the hash sign (`#`). +At the end, fill in your Slack details: | Field | Description | | ----- | ----------- | -| `Webhook` | The incoming webhook url which you have to setup on slack. (https://my.slack.com/services/new/incoming-webhook/) | -| `Username` | Optional username which can be on messages sent to slack. | -| `notify only broken builds` | Notify only about broken builds, when build events are marked to be sent.| +| **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. | +| **Username** | Optional username which can be on messages sent to slack. Fill this in if you want to change the username of the bot. | +| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. | +After you are all done, click **Save changes** for the changes to take effect. + +>**Note:** +You can set "branch,pushed,Compare changes" as highlight words on your Slack +profile settings, so that you can be aware of new commits when somebody pushes +them. + + +[slackhook]: https://my.slack.com/services/new/incoming-webhook diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md index 8fbcbb983e9..cf891cd90ad 100644 --- a/doc/raketasks/cleanup.md +++ b/doc/raketasks/cleanup.md @@ -2,7 +2,7 @@ ## Remove garbage from filesystem. Important! Data loss! -Remove namespaces(dirs) from `/home/git/repositories` if they don't exist in GitLab database. +Remove namespaces(dirs) from all repository storage paths if they don't exist in GitLab database. ``` # omnibus-gitlab @@ -12,7 +12,7 @@ sudo gitlab-rake gitlab:cleanup:dirs bundle exec rake gitlab:cleanup:dirs RAILS_ENV=production ``` -Rename repositories from `/home/git/repositories` if they don't exist in GitLab database. +Rename repositories from all repository storage paths if they don't exist in GitLab database. The repositories get a `+orphaned+TIMESTAMP` suffix so that they cannot block new repositories from being created. ``` diff --git a/doc/update/8.9-to-8.10.md b/doc/update/8.9-to-8.10.md index 84065a84e50..a057a423e61 100644 --- a/doc/update/8.9-to-8.10.md +++ b/doc/update/8.9-to-8.10.md @@ -46,7 +46,7 @@ sudo -u git -H git checkout 8-10-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all --tags -sudo -u git -H git checkout v3.2.0 +sudo -u git -H git checkout v3.2.1 ``` ### 5. Update gitlab-workhorse @@ -58,7 +58,7 @@ GitLab 8.1. ```bash cd /home/git/gitlab-workhorse sudo -u git -H git fetch --all -sudo -u git -H git checkout v0.7.7 +sudo -u git -H git checkout v0.7.8 sudo -u git -H make ``` diff --git a/doc/user/admin_area/settings/img/access_restrictions.png b/doc/user/admin_area/settings/img/access_restrictions.png Binary files differnew file mode 100644 index 00000000000..8eea84320d7 --- /dev/null +++ b/doc/user/admin_area/settings/img/access_restrictions.png diff --git a/doc/user/admin_area/settings/img/domain_blacklist.png b/doc/user/admin_area/settings/img/domain_blacklist.png Binary files differnew file mode 100644 index 00000000000..bd87b73cf9e --- /dev/null +++ b/doc/user/admin_area/settings/img/domain_blacklist.png diff --git a/doc/user/admin_area/settings/img/restricted_url.png b/doc/user/admin_area/settings/img/restricted_url.png Binary files differnew file mode 100644 index 00000000000..8b00a18320b --- /dev/null +++ b/doc/user/admin_area/settings/img/restricted_url.png diff --git a/doc/user/admin_area/settings/sign_up_restrictions.md b/doc/user/admin_area/settings/sign_up_restrictions.md new file mode 100644 index 00000000000..4b540473a6e --- /dev/null +++ b/doc/user/admin_area/settings/sign_up_restrictions.md @@ -0,0 +1,22 @@ +# Sign-up restrictions + +## Blacklist email domains + +> [Introduced][ce-5259] in GitLab 8.10. + +With this feature enabled, you can block email addresses of a specific domain +from creating an account on your GitLab server. This is particularly useful to +prevent spam. Disposable email addresses are usually used by malicious users to +create dummy accounts and spam issues. + +This feature can be activated via the **Application Settings** in the Admin area, +and you have the option of entering the list manually, or uploading a file with +the list. + +The blacklist accepts wildcards, so you can use `*.test.com` to block every +`test.com` subdomain, or `*.io` to block all domains ending in `.io`. Domains +should be separated by a whitespace, semicolon, comma, or a new line. + + + +[ce-5259]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5259 diff --git a/doc/administration/access_restrictions.md b/doc/user/admin_area/settings/visibility_and_access_controls.md index eb08cf139d4..633f16a617c 100644 --- a/doc/administration/access_restrictions.md +++ b/doc/user/admin_area/settings/visibility_and_access_controls.md @@ -1,6 +1,8 @@ -# Access Restrictions +# Visibility and access controls -> **Note:** These features are only available on versions 8.10 and above. +## Enabled Git access protocols + +> [Introduced][ce-4696] in GitLab 8.10. With GitLab's Access restrictions you can choose which Git access protocols you want your users to use to communicate with GitLab. This feature can be enabled @@ -15,8 +17,6 @@ to choose between:  -## Enabled Protocol - When both SSH and HTTP(S) are enabled, GitLab will behave as usual, it will give your users the option to choose which protocol they would like to use. @@ -37,20 +37,4 @@ not selected. HTTP, will still be accessible. What GitLab does is restrict access on the application level. -## Blacklist email domains - -With this feature enabled, you can block email addresses of a specific domain -from creating an account on your GitLab server. This is particularly useful to -prevent spam. Disposable email addresses are usually used by malicious users to -create dummy accounts and spam issues. - -This feature can be activated via the `Application Settings` in the Admin area, -and you have the option of entering the list manually, or uploading a file with -the list. - -The blacklist accepts wildcards, so you can use `*.test.com` to block every -`test.com` subdomain, or `*.io` to block all domains ending in `.io`. Domains -should be separated by a whitespace, semicolon, comma, or a new line. - - - +[ce-4696]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4696 diff --git a/doc/user/project/img/project_settings_list.png b/doc/user/project/img/project_settings_list.png Binary files differnew file mode 100644 index 00000000000..57ca2ac5f9e --- /dev/null +++ b/doc/user/project/img/project_settings_list.png diff --git a/doc/user/project/img/protected_branches_choose_branch.png b/doc/user/project/img/protected_branches_choose_branch.png Binary files differnew file mode 100644 index 00000000000..26328143717 --- /dev/null +++ b/doc/user/project/img/protected_branches_choose_branch.png diff --git a/doc/user/project/img/protected_branches_devs_can_push.png b/doc/user/project/img/protected_branches_devs_can_push.png Binary files differnew file mode 100644 index 00000000000..9c33db36586 --- /dev/null +++ b/doc/user/project/img/protected_branches_devs_can_push.png diff --git a/doc/user/project/img/protected_branches_error_ui.png b/doc/user/project/img/protected_branches_error_ui.png Binary files differnew file mode 100644 index 00000000000..cc61df7ca97 --- /dev/null +++ b/doc/user/project/img/protected_branches_error_ui.png diff --git a/doc/user/project/img/protected_branches_list.png b/doc/user/project/img/protected_branches_list.png Binary files differnew file mode 100644 index 00000000000..9f070f7a208 --- /dev/null +++ b/doc/user/project/img/protected_branches_list.png diff --git a/doc/user/project/img/protected_branches_matches.png b/doc/user/project/img/protected_branches_matches.png Binary files differnew file mode 100644 index 00000000000..30ce53f704e --- /dev/null +++ b/doc/user/project/img/protected_branches_matches.png diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md new file mode 100644 index 00000000000..6a8170b5ecb --- /dev/null +++ b/doc/user/project/protected_branches.md @@ -0,0 +1,106 @@ +# Protected Branches + +[Permissions](../permissions.md) in GitLab are fundamentally defined around the +idea of having read or write permission to the repository and branches. To +prevent people from messing with history or pushing code without review, we've +created protected branches. + +By default, a protected branch does four simple things: + +- it prevents its creation, if not already created, from everybody except users + with Master permission +- it prevents pushes from everybody except users with Master permission +- it prevents **anyone** from force pushing to the branch +- it prevents **anyone** from deleting the branch + +See the [Changelog](#changelog) section for changes over time. + +## Configuring protected branches + +To protect a branch, you need to have at least Master permission level. Note +that the `master` branch is protected by default. + +1. Navigate to the main page of the project. +1. In the upper right corner, click the settings wheel and select **Protected branches**. + +  + +1. From the **Branch** dropdown menu, select the branch you want to protect and + click **Protect**. In the screenshot below, we chose the `develop` branch. + +  + +1. Once done, the protected branch will appear in the "Already protected" list. + +  + + +Since GitLab 8.10, we added another layer of branch protection which provides +more granular management of protected branches. You can now choose the option +"Developers can merge" so that Developer users can merge a merge request but +not directly push. In that case, your branches are protected from direct pushes, +yet Developers don't need elevated permissions or wait for someone with a higher +permission level to press merge. + +You can set this option while creating the protected branch or after its +creation. + +## Wildcard protected branches + +>**Note:** +This feature was [introduced][ce-4665] in GitLab 8.10. + +You can specify a wildcard protected branch, which will protect all branches +matching the wildcard. For example: + +| Wildcard Protected Branch | Matching Branches | +|---------------------------+--------------------------------------------------------| +| `*-stable` | `production-stable`, `staging-stable` | +| `production/*` | `production/app-server`, `production/load-balancer` | +| `*gitlab*` | `gitlab`, `gitlab/staging`, `master/gitlab/production` | + +Protected branch settings (like "Developers can push") apply to all matching +branches. + +Two different wildcards can potentially match the same branch. For example, +`*-stable` and `production-*` would both match a `production-stable` branch. +In that case, if _any_ of these protected branches have a setting like +"Allowed to push", then `production-stable` will also inherit this setting. + +If you click on a protected branch's name that is created using a wildcard, +you will be presented with a list of all matching branches: + + + +## Restrict the creation of protected branches + +Creating a protected branch or a list of protected branches using the wildcard +feature, not only you are restricting pushes to those branches, but also their +creation if not already created. + +## Error messages when pushing to a protected branch + +A user with insufficient permissions will be presented with an error when +creating or pushing to a branch that's prohibited, either through GitLab's UI: + + + +or using Git from their terminal: + +```bash +remote: GitLab: You are not allowed to push code to protected branches on this project. +To https://gitlab.example.com/thedude/bowling.git + ! [remote rejected] staging-stable -> staging-stable (pre-receive hook declined) +error: failed to push some refs to 'https://gitlab.example.com/thedude/bowling.git' +``` + +## Changelog + +**8.10.0** + +- Allow specifying protected branches using wildcards [gitlab-org/gitlab-ce!5081][ce-4665] + +--- + +[ce-4665]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4665 "Allow specifying protected branches using wildcards" +[ce-5081]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5081 "Allow creating protected branches that can't be pushed to" diff --git a/doc/workflow/README.md b/doc/workflow/README.md index ddb2f7281b1..49dec613716 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -12,7 +12,7 @@ - [Project Features](project_features.md) - [Project forking workflow](forking_workflow.md) - [Project users](add-user/add-user.md) -- [Protected branches](protected_branches.md) +- [Protected branches](../user/project/protected_branches.md) - [Sharing a project with a group](share_with_group.md) - [Share projects with other groups](share_projects_with_other_groups.md) - [Web Editor](web_editor.md) diff --git a/doc/workflow/protected_branches.md b/doc/workflow/protected_branches.md index 5c1c7b47c8a..aa48b8f750e 100644 --- a/doc/workflow/protected_branches.md +++ b/doc/workflow/protected_branches.md @@ -1,55 +1 @@ -# Protected Branches - -Permissions in GitLab are fundamentally defined around the idea of having read or write permission to the repository and branches. - -To prevent people from messing with history or pushing code without review, we've created protected branches. - -A protected branch does three simple things: - -* it prevents pushes from everybody except users with Master permission -* it prevents anyone from force pushing to the branch -* it prevents anyone from deleting the branch - -You can make any branch a protected branch. GitLab makes the master branch a protected branch by default. - -To protect a branch, user needs to have at least a Master permission level, see [permissions document](../user/permissions.md). - - - -Navigate to project settings page and select `protected branches`. From the `Branch` dropdown menu select the branch you want to protect. - -Some workflows, like [GitLab workflow](gitlab_flow.md), require all users with write access to submit a Merge request in order to get the code into a protected branch. - -Since Masters and Owners can already push to protected branches, that means Developers cannot push to protected branch and need to submit a Merge request. - -However, there are workflows where that is not needed and only protecting from force pushes and branch removal is useful. - -For those workflows, you can allow everyone with write access to push to a protected branch by selecting `Developers can push` check box. - -On already protected branches you can also allow developers to push to the repository by selecting the `Developers can push` check box. - - - -## Wildcard Protected Branches - ->**Note:** -This feature was added in GitLab 8.10. - -1. You can specify a wildcard protected branch, which will protect all branches matching the wildcard. For example: - - | Wildcard Protected Branch | Matching Branches | - |---------------------------+--------------------------------------------------------| - | `*-stable` | `production-stable`, `staging-stable` | - | `production/*` | `production/app-server`, `production/load-balancer` | - | `*gitlab*` | `gitlab`, `gitlab/staging`, `master/gitlab/production` | - -1. Protected branch settings (like "Developers Can Push") apply to all matching branches. - -1. Two different wildcards can potentially match the same branch. For example, `*-stable` and `production-*` would both match a `production-stable` branch. - >**Note:** - If _any_ of these protected branches have "Developers Can Push" set to true, then `production-stable` has it set to true. - -1. If you click on a protected branch's name, you will be presented with a list of all matching branches: - -  - +This document is moved to [user/project/protected_branches.md](../user/project/protected_branches.md) diff --git a/doc/workflow/protected_branches/protected_branches1.png b/doc/workflow/protected_branches/protected_branches1.png Binary files differdeleted file mode 100644 index c00443803de..00000000000 --- a/doc/workflow/protected_branches/protected_branches1.png +++ /dev/null diff --git a/doc/workflow/protected_branches/protected_branches2.png b/doc/workflow/protected_branches/protected_branches2.png Binary files differdeleted file mode 100644 index a4f664d3b21..00000000000 --- a/doc/workflow/protected_branches/protected_branches2.png +++ /dev/null diff --git a/doc/workflow/protected_branches/protected_branches3.png b/doc/workflow/protected_branches/protected_branches3.png Binary files differdeleted file mode 100644 index 2a50cb174bb..00000000000 --- a/doc/workflow/protected_branches/protected_branches3.png +++ /dev/null diff --git a/features/project/wiki.feature b/features/project/wiki.feature index d4811b1ff54..63ce3ccb536 100644 --- a/features/project/wiki.feature +++ b/features/project/wiki.feature @@ -8,6 +8,12 @@ Feature: Project Wiki Given I create the Wiki Home page Then I should see the newly created wiki page + Scenario: Add new page with errors + Given I create the Wiki Home page with no content + Then I should see a "Content can't be blank" error message + When I create the Wiki Home page + Then I should see the newly created wiki page + Scenario: Pressing Cancel while editing a brand new Wiki Given I click on the Cancel button Then I should be redirected back to the Edit Home Wiki page diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index 3cbf832c728..732dc5d0b93 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -19,6 +19,11 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps click_on "Create page" end + step 'I create the Wiki Home page with no content' do + fill_in "wiki_content", with: '' + click_on "Create page" + end + step 'I should see the newly created wiki page' do expect(page).to have_content "Home" expect(page).to have_content "link test" @@ -125,15 +130,15 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps step 'I create a New page with paths' do click_on 'New Page' - fill_in 'Page slug', with: 'one/two/three' + fill_in 'Page slug', with: 'one/two/three-test' click_on 'Create Page' fill_in "wiki_content", with: 'wiki content' click_on "Create page" - expect(current_path).to include 'one/two/three' + expect(current_path).to include 'one/two/three-test' end step 'I should see non-escaped link in the pages list' do - expect(page).to have_xpath("//a[@href='/#{project.path_with_namespace}/wikis/one/two/three']") + expect(page).to have_xpath("//a[@href='/#{project.path_with_namespace}/wikis/one/two/three-test']") end step 'I edit the Wiki page with a path' do @@ -142,7 +147,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I should see a non-escaped path' do - expect(current_path).to include 'one/two/three' + expect(current_path).to include 'one/two/three-test' end step 'I should see the Editing page' do @@ -173,6 +178,11 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps find('a[href*="?version_id"]') end + step 'I should see a "Content can\'t be blank" error message' do + expect(page).to have_content('The form contains the following error:') + expect(page).to have_content('Content can\'t be blank') + end + def wiki @project_wiki = ProjectWiki.new(project, current_user) end diff --git a/lib/api/builds.rb b/lib/api/builds.rb index d36047acd1f..be5a3484ec8 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -52,8 +52,7 @@ module API get ':id/builds/:build_id' do authorize_read_builds! - build = get_build(params[:build_id]) - return not_found!(build) unless build + build = get_build!(params[:build_id]) present build, with: Entities::Build, user_can_download_artifacts: can?(current_user, :read_build, user_project) @@ -69,18 +68,27 @@ module API get ':id/builds/:build_id/artifacts' do authorize_read_builds! - build = get_build(params[:build_id]) - return not_found!(build) unless build + build = get_build!(params[:build_id]) - artifacts_file = build.artifacts_file + present_artifacts!(build.artifacts_file) + end - unless artifacts_file.file_storage? - return redirect_to build.artifacts_file.url - end + # Download the artifacts file from ref_name and job + # + # Parameters: + # id (required) - The ID of a project + # ref_name (required) - The ref from repository + # job (required) - The name for the build + # Example Request: + # GET /projects/:id/builds/artifacts/:ref_name/download?job=name + get ':id/builds/artifacts/:ref_name/download', + requirements: { ref_name: /.+/ } do + authorize_read_builds! - return not_found! unless artifacts_file.exists? + builds = user_project.latest_successful_builds_for(params[:ref_name]) + latest_build = builds.find_by!(name: params[:job]) - present_file!(artifacts_file.path, artifacts_file.filename) + present_artifacts!(latest_build.artifacts_file) end # Get a trace of a specific build of a project @@ -97,8 +105,7 @@ module API get ':id/builds/:build_id/trace' do authorize_read_builds! - build = get_build(params[:build_id]) - return not_found!(build) unless build + build = get_build!(params[:build_id]) header 'Content-Disposition', "infile; filename=\"#{build.id}.log\"" content_type 'text/plain' @@ -118,8 +125,7 @@ module API post ':id/builds/:build_id/cancel' do authorize_update_builds! - build = get_build(params[:build_id]) - return not_found!(build) unless build + build = get_build!(params[:build_id]) build.cancel @@ -137,8 +143,7 @@ module API post ':id/builds/:build_id/retry' do authorize_update_builds! - build = get_build(params[:build_id]) - return not_found!(build) unless build + build = get_build!(params[:build_id]) return forbidden!('Build is not retryable') unless build.retryable? build = Ci::Build.retry(build, current_user) @@ -157,8 +162,7 @@ module API post ':id/builds/:build_id/erase' do authorize_update_builds! - build = get_build(params[:build_id]) - return not_found!(build) unless build + build = get_build!(params[:build_id]) return forbidden!('Build is not erasable!') unless build.erasable? build.erase(erased_by: current_user) @@ -176,8 +180,8 @@ module API post ':id/builds/:build_id/artifacts/keep' do authorize_update_builds! - build = get_build(params[:build_id]) - return not_found!(build) unless build && build.artifacts? + build = get_build!(params[:build_id]) + return not_found!(build) unless build.artifacts? build.keep_artifacts! @@ -192,6 +196,20 @@ module API user_project.builds.find_by(id: id.to_i) end + def get_build!(id) + get_build(id) || not_found! + end + + def present_artifacts!(artifacts_file) + if !artifacts_file.file_storage? + redirect_to(build.artifacts_file.url) + elsif artifacts_file.exists? + present_file!(artifacts_file.path, artifacts_file.filename) + else + not_found! + end + end + def filter_builds(builds, scope) return builds if scope.nil? || scope.empty? diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index b9773f98d75..1f5917b8127 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -54,10 +54,10 @@ module Backup # Move repos dir to 'repositories.old' dir bk_repos_path = File.join(path, '..', 'repositories.old.' + Time.now.to_i.to_s) FileUtils.mv(path, bk_repos_path) + # This is expected from gitlab:check + FileUtils.mkdir_p(path, mode: 2770) end - FileUtils.mkdir_p(repos_path) - Project.find_each(batch_size: 1000) do |project| $progress.print " * #{project.path_with_namespace} ... " diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index 21ed0410f7f..337fb50317d 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -20,7 +20,7 @@ module Banzai process_link_attr el.attribute('href') end - doc.search('img').each do |el| + doc.css('img, video').each do |el| process_link_attr el.attribute('src') end diff --git a/lib/banzai/filter/video_link_filter.rb b/lib/banzai/filter/video_link_filter.rb new file mode 100644 index 00000000000..fd8b9a6f0cc --- /dev/null +++ b/lib/banzai/filter/video_link_filter.rb @@ -0,0 +1,59 @@ +module Banzai + module Filter + + # Find every image that isn't already wrapped in an `a` tag, and that has + # a `src` attribute ending with a video extension, add a new video node and + # a "Download" link in the case the video cannot be played. + class VideoLinkFilter < HTML::Pipeline::Filter + + def call + doc.xpath(query).each do |el| + el.replace(video_node(doc, el)) + end + + doc + end + + private + + def query + @query ||= begin + src_query = UploaderHelper::VIDEO_EXT.map do |ext| + "'.#{ext}' = substring(@src, string-length(@src) - #{ext.size})" + end + + "descendant-or-self::img[not(ancestor::a) and (#{src_query.join(' or ')})]" + end + end + + def video_node(doc, element) + container = doc.document.create_element( + 'div', + class: 'video-container' + ) + + video = doc.document.create_element( + 'video', + src: element['src'], + width: '400', + controls: true, + 'data-setup' => '{}') + + link = doc.document.create_element( + 'a', + element['title'] || element['alt'], + href: element['src'], + target: '_blank', + title: "Download '#{element['title'] || element['alt']}'") + download_paragraph = doc.document.create_element('p') + download_paragraph.children = link + + container.add_child(video) + container.add_child(download_paragraph) + + container + end + end + + end +end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index b27ecf3c923..8d94b199c66 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -7,6 +7,7 @@ module Banzai Filter::SanitizationFilter, Filter::UploadLinkFilter, + Filter::VideoLinkFilter, Filter::ImageLinkFilter, Filter::EmojiFilter, Filter::TableOfContentsFilter, diff --git a/lib/banzai/reference_extractor.rb b/lib/banzai/reference_extractor.rb index bf366962aef..b26a41a1f3b 100644 --- a/lib/banzai/reference_extractor.rb +++ b/lib/banzai/reference_extractor.rb @@ -2,11 +2,11 @@ module Banzai # Extract possible GFM references from an arbitrary String for further processing. class ReferenceExtractor def initialize - @texts = [] + @texts_and_contexts = [] end def analyze(text, context = {}) - @texts << Renderer.render(text, context) + @texts_and_contexts << { text: text, context: context } end def references(type, project, current_user = nil) @@ -21,9 +21,10 @@ module Banzai def html_documents # This ensures that we don't memoize anything until we have a number of # text blobs to parse. - return [] if @texts.empty? + return [] if @texts_and_contexts.empty? - @html_documents ||= @texts.map { |html| Nokogiri::HTML.fragment(html) } + @html_documents ||= Renderer.cache_collection_render(@texts_and_contexts) + .map { |html| Nokogiri::HTML.fragment(html) } end end end diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb index de41ea415a6..a533bac2692 100644 --- a/lib/gitlab/access.rb +++ b/lib/gitlab/access.rb @@ -7,6 +7,7 @@ module Gitlab module Access class AccessDeniedError < StandardError; end + NO_ACCESS = 0 GUEST = 10 REPORTER = 20 DEVELOPER = 30 diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 34e0143a82e..839a4fa30d5 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -60,16 +60,18 @@ module Gitlab end # Fork repository to new namespace - # storage - project's storage path + # forked_from_storage - forked-from project's storage path # path - project path with namespace + # forked_to_storage - forked-to project's storage path # fork_namespace - namespace for forked project # # Ex. - # fork_repository("/path/to/storage", "gitlab/gitlab-ci", "randx") + # fork_repository("/path/to/forked_from/storage", "gitlab/gitlab-ci", "/path/to/forked_to/storage", "randx") # - def fork_repository(storage, path, fork_namespace) + def fork_repository(forked_from_storage, path, forked_to_storage, fork_namespace) Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'fork-project', - storage, "#{path}.git", fork_namespace]) + forked_from_storage, "#{path}.git", forked_to_storage, + fork_namespace]) end # Remove repository from file system diff --git a/lib/gitlab/diff/parallel_diff.rb b/lib/gitlab/diff/parallel_diff.rb index b069afdd28c..481536a380b 100644 --- a/lib/gitlab/diff/parallel_diff.rb +++ b/lib/gitlab/diff/parallel_diff.rb @@ -8,72 +8,35 @@ module Gitlab end def parallelize - i = 0 free_right_index = nil lines = [] highlighted_diff_lines = diff_file.highlighted_diff_lines highlighted_diff_lines.each do |line| - line_code = diff_file.line_code(line) - position = diff_file.position(line) - - case line.type - when 'match', nil + if line.meta? || line.unchanged? # line in the right panel is the same as in the left one lines << { - left: { - type: line.type, - number: line.old_pos, - text: line.text, - line_code: line_code, - position: position - }, - right: { - type: line.type, - number: line.new_pos, - text: line.text, - line_code: line_code, - position: position - } + left: line, + right: line } free_right_index = nil i += 1 - when 'old' + elsif line.removed? lines << { - left: { - type: line.type, - number: line.old_pos, - text: line.text, - line_code: line_code, - position: position - }, - right: { - type: nil, - number: nil, - text: "", - line_code: line_code, - position: position - } + left: line, + right: nil } # Once we come upon a new line it can be put on the right of this old line free_right_index ||= i i += 1 - when 'new' - data = { - type: line.type, - number: line.new_pos, - text: line.text, - line_code: line_code, - position: position - } - + elsif line.added? if free_right_index # If an old line came before this without a line on the right, this # line can be put to the right of it. - lines[free_right_index][:right] = data + lines[free_right_index][:right] = line # If there are any other old lines on the left that don't yet have # a new counterpart on the right, update the free_right_index @@ -81,14 +44,8 @@ module Gitlab free_right_index = next_free_right_index < i ? next_free_right_index : nil else lines << { - left: { - type: nil, - number: nil, - text: "", - line_code: line_code, - position: position - }, - right: data + left: nil, + right: line } free_right_index = nil diff --git a/lib/gitlab/request_profiler.rb b/lib/gitlab/request_profiler.rb new file mode 100644 index 00000000000..8130e55351e --- /dev/null +++ b/lib/gitlab/request_profiler.rb @@ -0,0 +1,19 @@ +require 'fileutils' + +module Gitlab + module RequestProfiler + PROFILES_DIR = "#{Gitlab.config.shared.path}/tmp/requests_profiles" + + def profile_token + Rails.cache.fetch('profile-token') do + Devise.friendly_token + end + end + module_function :profile_token + + def remove_all_profiles + FileUtils.rm_rf(PROFILES_DIR) + end + module_function :remove_all_profiles + end +end diff --git a/lib/gitlab/request_profiler/middleware.rb b/lib/gitlab/request_profiler/middleware.rb new file mode 100644 index 00000000000..8da8b754975 --- /dev/null +++ b/lib/gitlab/request_profiler/middleware.rb @@ -0,0 +1,47 @@ +require 'ruby-prof' + +module Gitlab + module RequestProfiler + class Middleware + def initialize(app) + @app = app + end + + def call(env) + if profile?(env) + call_with_profiling(env) + else + @app.call(env) + end + end + + def profile?(env) + header_token = env['HTTP_X_PROFILE_TOKEN'] + return unless header_token.present? + + profile_token = RequestProfiler.profile_token + return unless profile_token.present? + + header_token == profile_token + end + + def call_with_profiling(env) + ret = nil + result = RubyProf::Profile.profile do + ret = @app.call(env) + end + + printer = RubyProf::CallStackPrinter.new(result) + file_name = "#{env['PATH_INFO'].tr('/', '|')}_#{Time.current.to_i}.html" + file_path = "#{PROFILES_DIR}/#{file_name}" + + FileUtils.mkdir_p(PROFILES_DIR) + File.open(file_path, 'wb') do |file| + printer.print(file) + end + + ret + end + end + end +end diff --git a/lib/gitlab/request_profiler/profile.rb b/lib/gitlab/request_profiler/profile.rb new file mode 100644 index 00000000000..f89d56903ef --- /dev/null +++ b/lib/gitlab/request_profiler/profile.rb @@ -0,0 +1,43 @@ +module Gitlab + module RequestProfiler + class Profile + attr_reader :name, :time, :request_path + + alias_method :to_param, :name + + def self.all + Dir["#{PROFILES_DIR}/*.html"].map do |path| + new(File.basename(path)) + end + end + + def self.find(name) + name_dup = name.dup + name_dup << '.html' unless name.end_with?('.html') + + file_path = "#{PROFILES_DIR}/#{name_dup}" + return unless File.exist?(file_path) + + new(name_dup) + end + + def initialize(name) + @name = name + + set_attributes + end + + def content + File.read("#{PROFILES_DIR}/#{name}") + end + + private + + def set_attributes + _, path, timestamp = name.split(/(.*)_(\d+)\.html$/) + @request_path = path.tr('|', '/') + @time = Time.at(timestamp.to_i).utc + end + end + end +end diff --git a/lib/gitlab/sidekiq_middleware/request_store_middleware.rb b/lib/gitlab/sidekiq_middleware/request_store_middleware.rb new file mode 100644 index 00000000000..b1fa0e3cb4e --- /dev/null +++ b/lib/gitlab/sidekiq_middleware/request_store_middleware.rb @@ -0,0 +1,13 @@ +module Gitlab + module SidekiqMiddleware + class RequestStoreMiddleware + def call(worker, job, queue) + RequestStore.begin! + yield + ensure + RequestStore.end! + RequestStore.clear! + end + end + end +end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 6aeb49c0219..c6826a09bd2 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -4,6 +4,7 @@ require 'json' module Gitlab class Workhorse SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data' + VERSION_FILE = 'GITLAB_WORKHORSE_VERSION' class << self def git_http_ok(repository, user) @@ -75,6 +76,11 @@ module Gitlab ] end + def version + path = Rails.root.join(VERSION_FILE) + path.readable? ? path.read.chomp : 'unknown' + end + protected def encode(hash) diff --git a/lib/repository_cache.rb b/lib/repository_cache.rb index 8ddc3511293..068a95790c0 100644 --- a/lib/repository_cache.rb +++ b/lib/repository_cache.rb @@ -1,14 +1,15 @@ # Interface to the Redis-backed cache store used by the Repository model class RepositoryCache - attr_reader :namespace, :backend + attr_reader :namespace, :backend, :project_id - def initialize(namespace, backend = Rails.cache) + def initialize(namespace, project_id, backend = Rails.cache) @namespace = namespace @backend = backend + @project_id = project_id end def cache_key(type) - "#{type}:#{namespace}" + "#{type}:#{namespace}:#{project_id}" end def expire(key) diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index 0ec19e1a625..7c96bc864ce 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -25,6 +25,10 @@ namespace :gitlab do desc 'Drop all tables' task :drop_tables => :environment do connection = ActiveRecord::Base.connection + + # If MySQL, turn off foreign key checks + connection.execute('SET FOREIGN_KEY_CHECKS=0') if Gitlab::Database.mysql? + tables = connection.tables tables.delete 'schema_migrations' # Truncate schema_migrations to ensure migrations re-run @@ -35,6 +39,9 @@ namespace :gitlab do # MySQL: http://dev.mysql.com/doc/refman/5.7/en/drop-table.html # Add `IF EXISTS` because cascade could have already deleted a table. tables.each { |t| connection.execute("DROP TABLE IF EXISTS #{connection.quote_table_name(t)} CASCADE") } + + # If MySQL, re-enable foreign key checks + connection.execute('SET FOREIGN_KEY_CHECKS=1') if Gitlab::Database.mysql? end desc 'Configures the database by running migrate, or by loading the schema and seeding if needed' diff --git a/public/404.html b/public/404.html index 4862770cc2a..92b7f4da0b9 100644 --- a/public/404.html +++ b/public/404.html @@ -1,55 +1,65 @@ <!DOCTYPE html> <html> <head> + <meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport"> <title>The page you're looking for could not be found (404)</title> <style> - body { - color: #666; - text-align: center; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - margin: 0; - width: 800px; - margin: auto; - font-size: 14px; - } + body { + color: #666; + text-align: center; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + margin: auto; + font-size: 14px; + } - h1 { - font-size: 56px; - line-height: 100px; - font-weight: normal; - color: #456; - } + h1 { + font-size: 56px; + line-height: 100px; + font-weight: normal; + color: #456; + } - h2 { - font-size: 24px; - color: #666; - line-height: 1.5em; - } + h2 { + font-size: 24px; + color: #666; + line-height: 1.5em; + } - h3 { - color: #456; - font-size: 20px; - font-weight: normal; - line-height: 28px; - } + h3 { + color: #456; + font-size: 20px; + font-weight: normal; + line-height: 28px; + } - hr { - margin: 18px 0; - border: 0; - border-top: 1px solid #EEE; - border-bottom: 1px solid white; - } + hr { + max-width: 800px; + margin: 18px auto; + border: 0; + border-top: 1px solid #EEE; + border-bottom: 1px solid white; + } + + img { + max-width: 40vw; + } + + .container { + margin: auto 20px; + } </style> </head> <body> <h1> - <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br /> + <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br /> 404 </h1> - <h3>The page you're looking for could not be found.</h3> - <hr/> - <p>Make sure the address is correct and that the page hasn't moved.</p> - <p>Please contact your GitLab administrator if you think this is a mistake.</p> + <div class="container"> + <h3>The page you're looking for could not be found.</h3> + <hr /> + <p>Make sure the address is correct and that the page hasn't moved.</p> + <p>Please contact your GitLab administrator if you think this is a mistake.</p> + </div> </body> </html> diff --git a/public/422.html b/public/422.html index 055b0bde165..f625f8a33b7 100644 --- a/public/422.html +++ b/public/422.html @@ -1,55 +1,65 @@ <!DOCTYPE html> <html> <head> + <meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport"> <title>The change you requested was rejected (422)</title> <style> body { color: #666; - text-align: center; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - margin: 0; - width: 800px; - margin: auto; - font-size: 14px; - } + text-align: center; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + margin: auto; + font-size: 14px; + } - h1 { - font-size: 56px; - line-height: 100px; - font-weight: normal; - color: #456; - } + h1 { + font-size: 56px; + line-height: 100px; + font-weight: normal; + color: #456; + } - h2 { - font-size: 24px; - color: #666; - line-height: 1.5em; - } + h2 { + font-size: 24px; + color: #666; + line-height: 1.5em; + } + + h3 { + color: #456; + font-size: 20px; + font-weight: normal; + line-height: 28px; + } + + hr { + max-width: 800px; + margin: 18px auto; + border: 0; + border-top: 1px solid #EEE; + border-bottom: 1px solid white; + } - h3 { - color: #456; - font-size: 20px; - font-weight: normal; - line-height: 28px; - } + img { + max-width: 40vw; + } - hr { - margin: 18px 0; - border: 0; - border-top: 1px solid #EEE; - border-bottom: 1px solid white; - } - </style> + .container { + margin: auto 20px; + } + </style> </head> <body> <h1> - <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br /> + <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br /> 422 </h1> - <h3>The change you requested was rejected.</h3> - <hr /> - <p>Make sure you have access to the thing you tried to change.</p> - <p>Please contact your GitLab administrator if you think this is a mistake.</p> + <div class="container"> + <h3>The change you requested was rejected.</h3> + <hr /> + <p>Make sure you have access to the thing you tried to change.</p> + <p>Please contact your GitLab administrator if you think this is a mistake.</p> + </div> </body> </html> diff --git a/public/500.html b/public/500.html index 3d59d1392f5..d76c66ba92a 100644 --- a/public/500.html +++ b/public/500.html @@ -1,54 +1,65 @@ <!DOCTYPE html> <html> <head> + <meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport"> <title>Something went wrong (500)</title> <style> - body { - color: #666; - text-align: center; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - margin: 0; - width: 800px; - margin: auto; - font-size: 14px; - } + body { + color: #666; + text-align: center; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + margin: auto; + font-size: 14px; + } - h1 { - font-size: 56px; - line-height: 100px; - font-weight: normal; - color: #456; - } + h1 { + font-size: 56px; + line-height: 100px; + font-weight: normal; + color: #456; + } - h2 { - font-size: 24px; - color: #666; - line-height: 1.5em; - } + h2 { + font-size: 24px; + color: #666; + line-height: 1.5em; + } - h3 { - color: #456; - font-size: 20px; - font-weight: normal; - line-height: 28px; - } + h3 { + color: #456; + font-size: 20px; + font-weight: normal; + line-height: 28px; + } - hr { - margin: 18px 0; + hr { + max-width: 800px; + margin: 18px auto; border: 0; border-top: 1px solid #EEE; border-bottom: 1px solid white; } + + img { + max-width: 40vw; + } + + .container { + margin: auto 20px; + } </style> </head> + <body> <h1> - <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br /> + <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br /> 500 </h1> - <h3>Whoops, something went wrong on our end.</h3> - <hr/> - <p>Try refreshing the page, or going back and attempting the action again.</p> - <p>Please contact your GitLab administrator if this problem persists.</p> + <div class="container"> + <h3>Whoops, something went wrong on our end.</h3> + <hr /> + <p>Try refreshing the page, or going back and attempting the action again.</p> + <p>Please contact your GitLab administrator if this problem persists.</p> + </div> </body> </html> diff --git a/public/502.html b/public/502.html index 67dfd8a2743..1a3c7efc769 100644 --- a/public/502.html +++ b/public/502.html @@ -1,14 +1,13 @@ <!DOCTYPE html> <html> <head> + <meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport"> <title>GitLab is not responding (502)</title> <style> body { color: #666; text-align: center; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - margin: 0; - width: 800px; margin: auto; font-size: 14px; } @@ -34,21 +33,33 @@ } hr { - margin: 18px 0; + max-width: 800px; + margin: 18px auto; border: 0; border-top: 1px solid #EEE; border-bottom: 1px solid white; } + + img { + max-width: 40vw; + } + + .container { + margin: auto 20px; + } </style> </head> + <body> <h1> - <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br /> + <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br /> 502 </h1> - <h3>Whoops, GitLab is taking too much time to respond.</h3> - <hr/> - <p>Try refreshing the page, or going back and attempting the action again.</p> - <p>Please contact your GitLab administrator if this problem persists.</p> + <div class="container"> + <h3>Whoops, GitLab is taking too much time to respond.</h3> + <hr /> + <p>Try refreshing the page, or going back and attempting the action again.</p> + <p>Please contact your GitLab administrator if this problem persists.</p> + </div> </body> </html> diff --git a/public/503.html b/public/503.html index 6ab1185658d..c1c4e3ffdb8 100644 --- a/public/503.html +++ b/public/503.html @@ -1,14 +1,13 @@ <!DOCTYPE html> <html> <head> + <meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport"> <title>GitLab is not responding (503)</title> <style> body { color: #666; text-align: center; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - margin: 0; - width: 800px; margin: auto; font-size: 14px; } @@ -34,21 +33,33 @@ } hr { - margin: 18px 0; + max-width: 800px; + margin: 18px auto; border: 0; border-top: 1px solid #EEE; border-bottom: 1px solid white; } + + img { + max-width: 40vw; + } + + .container { + margin: auto 20px; + } </style> </head> + <body> <h1> - <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo"/><br /> + <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br /> 503 </h1> - <h3>Whoops, GitLab is currently unavailable.</h3> - <hr/> - <p>Try refreshing the page, or going back and attempting the action again.</p> - <p>Please contact your GitLab administrator if this problem persists.</p> + <div class="container"> + <h3>Whoops, GitLab is currently unavailable.</h3> + <hr /> + <p>Try refreshing the page, or going back and attempting the action again.</p> + <p>Please contact your GitLab administrator if this problem persists.</p> + </div> </body> </html> diff --git a/public/deploy.html b/public/deploy.html index 48976dacf41..142472b6c35 100644 --- a/public/deploy.html +++ b/public/deploy.html @@ -1,54 +1,64 @@ <!DOCTYPE html> <html> - <head> - <title>Deploy in progress</title> - <style> - body { - color: #666; - text-align: center; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - margin: 0; - width: 800px; - margin: auto; - font-size: 14px; - } +<head> + <meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport"> + <title>Deploy in progress</title> + <style> + body { + color: #666; + text-align: center; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + margin: auto; + font-size: 14px; + } - h1 { - font-size: 56px; - line-height: 100px; - font-weight: normal; - color: #456; - } + h1 { + font-size: 56px; + line-height: 100px; + font-weight: normal; + color: #456; + } - h2 { - font-size: 24px; - color: #666; - line-height: 1.5em; - } + h2 { + font-size: 24px; + color: #666; + line-height: 1.5em; + } - h3 { - color: #456; - font-size: 20px; - font-weight: normal; - line-height: 28px; - } + h3 { + color: #456; + font-size: 20px; + font-weight: normal; + line-height: 28px; + } - hr { - margin: 18px 0; - border: 0; - border-top: 1px solid #EEE; - border-bottom: 1px solid white; - } - </style> - </head> + hr { + max-width: 800px; + margin: 18px auto; + border: 0; + border-top: 1px solid #EEE; + border-bottom: 1px solid white; + } - <body> - <h1> - <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br /> - Deploy in progress - </h1> + img { + max-width: 40vw; + } + + .container { + margin: auto 20px; + } + </style> +</head> + +<body> + <h1> + <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br /> + Deploy in progress + </h1> + <div class="container"> <h3>Please try again in a few minutes.</h3> - <hr/> + <hr /> <p>Please contact your GitLab administrator if this problem persists.</p> - </body> + </div> +</body> </html> diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 60c654f622d..ed0b7f9e240 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -163,4 +163,17 @@ describe AutocompleteController do expect(body.collect { |u| u['id'] }).not_to include(99999) end end + + context 'skip_users parameter included' do + before { sign_in(user) } + + it 'skips the user IDs passed' do + get(:users, skip_users: [user, user2].map(&:id)) + + other_user_ids = [non_member, project.owner, project.creator].map(&:id) + response_user_ids = JSON.parse(response.body).map { |user| user['id'] } + + expect(response_user_ids).to contain_exactly(*other_user_ids) + end + end end diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb index 347bef1e129..33c75e7584f 100644 --- a/spec/controllers/help_controller_spec.rb +++ b/spec/controllers/help_controller_spec.rb @@ -36,7 +36,7 @@ describe HelpController do context 'when requested file exists' do it 'renders the raw file' do get :show, - path: 'workflow/protected_branches/protected_branches1', + path: 'user/project/img/labels_filter', format: :png expect(response).to be_success expect(response.content_type).to eq 'image/png' diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb new file mode 100644 index 00000000000..a6995145cc1 --- /dev/null +++ b/spec/controllers/projects/tags_controller_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Projects::TagsController do + let(:project) { create(:project, :public) } + let!(:release) { create(:release, project: project) } + let!(:invalid_release) { create(:release, project: project, tag: 'does-not-exist') } + + describe 'GET index' do + before { get :index, namespace_id: project.namespace.to_param, project_id: project.to_param } + + it 'returns the tags for the page' do + expect(assigns(:tags).map(&:name)).to eq(['v1.1.0', 'v1.0.0']) + end + + it 'returns releases matching those tags' do + expect(assigns(:releases)).to include(release) + expect(assigns(:releases)).not_to include(invalid_release) + end + end +end diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb index 0893ee89f6a..71d0e4be834 100644 --- a/spec/controllers/projects/uploads_controller_spec.rb +++ b/spec/controllers/projects/uploads_controller_spec.rb @@ -14,9 +14,9 @@ describe Projects::UploadsController do context "without params['file']" do it "returns an error" do - post :create, + post :create, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project.to_param, format: :json expect(response).to have_http_status(422) end @@ -34,23 +34,21 @@ describe Projects::UploadsController do it 'returns a content with original filename, new link, and correct type.' do expect(response.body).to match '\"alt\":\"rails_sample\"' expect(response.body).to match "\"url\":\"/uploads" - expect(response.body).to match '\"is_image\":true' end end context 'with valid non-image file' do before do - post :create, + post :create, namespace_id: project.namespace.to_param, - project_id: project.to_param, - file: txt, + project_id: project.to_param, + file: txt, format: :json end it 'returns a content with original filename, new link, and correct type.' do expect(response.body).to match '\"alt\":\"doc_sample.txt\"' expect(response.body).to match "\"url\":\"/uploads" - expect(response.body).to match '\"is_image\":false' end end end diff --git a/spec/factories/ci/trigger_requests.rb b/spec/factories/ci/trigger_requests.rb index 6d47d05f8ad..b8d8fab0e0b 100644 --- a/spec/factories/ci/trigger_requests.rb +++ b/spec/factories/ci/trigger_requests.rb @@ -5,7 +5,8 @@ FactoryGirl.define do variables do { - TRIGGER_KEY: 'TRIGGER_VALUE' + TRIGGER_KEY_1: 'TRIGGER_VALUE_1', + TRIGGER_KEY_2: 'TRIGGER_VALUE_2' } end end diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb index e72aa9479b7..2c0a2dd94ca 100644 --- a/spec/factories/issues.rb +++ b/spec/factories/issues.rb @@ -18,5 +18,15 @@ FactoryGirl.define do factory :closed_issue, traits: [:closed] factory :reopened_issue, traits: [:reopened] + + factory :labeled_issue do + transient do + labels [] + end + + after(:create) do |issue, evaluator| + issue.update_attributes(labels: evaluator.labels) + end + end end end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index cab3dc1d167..0cfeb2e57d8 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -199,9 +199,13 @@ describe "Builds" do click_link 'Retry' end - it { expect(page.status_code).to eq(200) } - it { expect(page).to have_content 'pending' } - it { expect(page).to have_content 'Cancel' } + it 'shows the right status and buttons' do + expect(page).to have_http_status(200) + expect(page).to have_content 'pending' + page.within('aside.right-sidebar') do + expect(page).to have_content 'Cancel' + end + end end context "Build from other project" do @@ -212,7 +216,25 @@ describe "Builds" do page.driver.post(retry_namespace_project_build_path(@project.namespace, @project, @build2)) end - it { expect(page.status_code).to eq(404) } + it { expect(page).to have_http_status(404) } + end + + context "Build that current user is not allowed to retry" do + before do + @build.run! + @build.cancel! + @project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + + logout_direct + login_with(create(:user)) + visit namespace_project_build_path(@project.namespace, @project, @build) + end + + it 'does not show the Retry button' do + page.within('aside.right-sidebar') do + expect(page).not_to have_content 'Retry' + end + end end end diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index 9c018be14b7..a7d9f2a0c72 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -92,8 +92,8 @@ feature 'Environments', feature: true do expect(page).to have_link(deployment.short_sha) end - scenario 'does not show a retry button for deployment without build' do - expect(page).not_to have_link('Retry') + scenario 'does not show a re-deploy button for deployment without build' do + expect(page).not_to have_link('Re-deploy') end context 'with build' do @@ -105,8 +105,8 @@ feature 'Environments', feature: true do expect(page).to have_link("#{build.name} (##{build.id})") end - scenario 'does show retry button' do - expect(page).to have_link('Retry') + scenario 'does show re-deploy button' do + expect(page).to have_link('Re-deploy') end context 'with manual action' do diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb index 5ea02b8d39c..cb117d2476f 100644 --- a/spec/features/issues/filter_by_labels_spec.rb +++ b/spec/features/issues/filter_by_labels_spec.rb @@ -205,7 +205,7 @@ feature 'Issue filtering by Labels', feature: true do page.within '.labels-filter' do click_button 'Label' wait_for_ajax - fill_in 'label-name', with: 'bug' + find('.dropdown-input input').set 'bug' page.within '.dropdown-content' do expect(page).not_to have_content 'enhancement' diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index 09ccc77c101..32159559c37 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -236,6 +236,14 @@ describe 'GitLab Markdown', feature: true do it 'includes TaskListFilter' do expect(doc).to parse_task_lists end + + it 'includes InlineDiffFilter' do + expect(doc).to parse_inline_diffs + end + + it 'includes VideoLinkFilter' do + expect(doc).to parse_video_links + end end context 'wiki pipeline' do @@ -293,6 +301,10 @@ describe 'GitLab Markdown', feature: true do it 'includes InlineDiffFilter' do expect(doc).to parse_inline_diffs end + + it 'includes VideoLinkFilter' do + expect(doc).to parse_video_links + end end # Fake a `current_user` helper diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb new file mode 100644 index 00000000000..79abba21854 --- /dev/null +++ b/spec/features/projects/branches_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe 'Branches', feature: true do + let(:project) { create(:project) } + let(:repository) { project.repository } + + before do + login_as :user + project.team << [@user, :developer] + end + + describe 'Initial branches page' do + it 'shows all the branches' do + visit namespace_project_branches_path(project.namespace, project) + + repository.branches { |branch| expect(page).to have_content("#{branch.name}") } + expect(page).to have_content("Protected branches can be managed in project settings") + end + end + + describe 'Find branches' do + it 'shows filtered branches', js: true do + visit namespace_project_branches_path(project.namespace, project, project.id) + + fill_in 'branch-search', with: 'fix' + find('#branch-search').native.send_keys(:enter) + + expect(page).to have_content('fix') + expect(find('.all-branches')).to have_selector('li', count: 1) + end + end +end diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index bc3bf53fe9d..2d1e3bbebe5 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -59,6 +59,21 @@ feature 'project import', feature: true, js: true do end end + scenario 'project with no name' do + create(:project, namespace_id: 2) + + visit new_project_path + + select2('2', from: '#project_namespace_id') + + # click on disabled element + find(:link, 'GitLab export').trigger('click') + + page.within('.flash-container') do + expect(page).to have_content('Please enter path and name') + end + end + def wiki_exists? wiki = ProjectWiki.new(project) File.exist?(wiki.repository.path_to_repo) && !wiki.repository.empty? diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb new file mode 100644 index 00000000000..a1c386ddc18 --- /dev/null +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -0,0 +1,140 @@ +require 'spec_helper' + +feature 'Projects > Wiki > User previews markdown changes', feature: true, js: true do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let(:wiki_content) do + <<-HEREDOC +[regular link](regular) +[relative link 1](../relative) +[relative link 2](./relative) +[relative link 3](./e/f/relative) + HEREDOC + end + + background do + project.team << [user, :master] + login_as(user) + + visit namespace_project_path(project.namespace, project) + click_link 'Wiki' + WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute + end + + context "while creating a new wiki page" do + context "when there are no spaces or hyphens in the page name" do + it "rewrites relative links as expected" do + click_link 'New Page' + fill_in :new_wiki_path, with: 'a/b/c/d' + click_button 'Create Page' + + fill_in :wiki_content, with: wiki_content + click_on "Preview" + + expect(page).to have_content("regular link") + + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/relative\">relative link 1</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/c/relative\">relative link 2</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/c/e/f/relative\">relative link 3</a>") + end + end + + context "when there are spaces in the page name" do + it "rewrites relative links as expected" do + click_link 'New Page' + fill_in :new_wiki_path, with: 'a page/b page/c page/d page' + click_button 'Create Page' + + fill_in :wiki_content, with: wiki_content + click_on "Preview" + + expect(page).to have_content("regular link") + + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/relative\">relative link 1</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") + end + end + + context "when there are hyphens in the page name" do + it "rewrites relative links as expected" do + click_link 'New Page' + fill_in :new_wiki_path, with: 'a-page/b-page/c-page/d-page' + click_button 'Create Page' + + fill_in :wiki_content, with: wiki_content + click_on "Preview" + + expect(page).to have_content("regular link") + + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/relative\">relative link 1</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") + end + end + end + + context "while editing a wiki page" do + def create_wiki_page(path) + click_link 'New Page' + fill_in :new_wiki_path, with: path + click_button 'Create Page' + fill_in :wiki_content, with: 'content' + click_on "Create page" + end + + context "when there are no spaces or hyphens in the page name" do + it "rewrites relative links as expected" do + create_wiki_page 'a/b/c/d' + click_link 'Edit' + + fill_in :wiki_content, with: wiki_content + click_on "Preview" + + expect(page).to have_content("regular link") + + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/relative\">relative link 1</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/c/relative\">relative link 2</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/c/e/f/relative\">relative link 3</a>") + end + end + + context "when there are spaces in the page name" do + it "rewrites relative links as expected" do + create_wiki_page 'a page/b page/c page/d page' + click_link 'Edit' + + fill_in :wiki_content, with: wiki_content + click_on "Preview" + + expect(page).to have_content("regular link") + + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/relative\">relative link 1</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") + end + end + + context "when there are hyphens in the page name" do + it "rewrites relative links as expected" do + create_wiki_page 'a-page/b-page/c-page/d-page' + click_link 'Edit' + + fill_in :wiki_content, with: wiki_content + click_on "Preview" + + expect(page).to have_content("regular link") + + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/relative\">relative link 1</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") + end + end + end +end diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index 7e6eef65873..7afd83b7250 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -30,18 +30,48 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute end - scenario 'via the "new wiki page" page', js: true do - click_link 'New Page' + context 'via the "new wiki page" page' do + scenario 'when the wiki page has a single word name', js: true do + click_link 'New Page' - fill_in :new_wiki_path, with: 'foo' - click_button 'Create Page' + fill_in :new_wiki_path, with: 'foo' + click_button 'Create Page' - fill_in :wiki_content, with: 'My awesome wiki!' - click_button 'Create page' + fill_in :wiki_content, with: 'My awesome wiki!' + click_button 'Create page' - expect(page).to have_content('Foo') - expect(page).to have_content("last edited by #{user.name}") - expect(page).to have_content('My awesome wiki!') + expect(page).to have_content('Foo') + expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') + end + + scenario 'when the wiki page has spaces in the name', js: true do + click_link 'New Page' + + fill_in :new_wiki_path, with: 'Spaces in the name' + click_button 'Create Page' + + fill_in :wiki_content, with: 'My awesome wiki!' + click_button 'Create page' + + expect(page).to have_content('Spaces in the name') + expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') + end + + scenario 'when the wiki page has hyphens in the name', js: true do + click_link 'New Page' + + fill_in :new_wiki_path, with: 'hyphens-in-the-name' + click_button 'Create Page' + + fill_in :wiki_content, with: 'My awesome wiki!' + click_button 'Create page' + + expect(page).to have_content('Hyphens in the name') + expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') + end end end end diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb new file mode 100644 index 00000000000..dd85203a038 --- /dev/null +++ b/spec/finders/branches_finder_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' + +describe BranchesFinder do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:repository) { project.repository } + + describe '#execute' do + context 'sort only' do + it 'sorts by name' do + branches_finder = described_class.new(repository, {}) + + result = branches_finder.execute + + expect(result.first.name).to eq("'test'") + end + + it 'sorts by recently_updated' do + branches_finder = described_class.new(repository, { sort: 'recently_updated' }) + + result = branches_finder.execute + + expect(result.first.name).to eq('video') + end + + it 'sorts by last_updated' do + branches_finder = described_class.new(repository, { sort: 'last_updated' }) + + result = branches_finder.execute + + expect(result.first.name).to eq('feature') + end + end + + context 'filter only' do + it 'filters branches by name' do + branches_finder = described_class.new(repository, { search: 'fix' }) + + result = branches_finder.execute + + expect(result.first.name).to eq('fix') + expect(result.count).to eq(1) + end + + it 'does not find any branch with that name' do + branches_finder = described_class.new(repository, { search: 'random' }) + + result = branches_finder.execute + + expect(result.count).to eq(0) + end + end + + context 'filter and sort' do + it 'filters branches by name and sorts by recently_updated' do + params = { sort: 'recently_updated', search: 'feature' } + branches_finder = described_class.new(repository, params) + + result = branches_finder.execute + + expect(result.first.name).to eq('feature_conflict') + expect(result.count).to eq(2) + end + + it 'filters branches by name and sorts by last_updated' do + params = { sort: 'last_updated', search: 'feature' } + branches_finder = described_class.new(repository, params) + + result = branches_finder.execute + + expect(result.first.name).to eq('feature') + expect(result.count).to eq(2) + end + end + end +end diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index c75d28d9801..f3e7c2d1a9f 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -256,3 +256,7 @@ However the wrapping tags can not be mixed as such - - [+ additions +} - {- delletions -] - [- delletions -} + +### Videos + + diff --git a/spec/fixtures/parallel_diff_result.yml b/spec/fixtures/parallel_diff_result.yml deleted file mode 100644 index 37066c8e930..00000000000 --- a/spec/fixtures/parallel_diff_result.yml +++ /dev/null @@ -1,800 +0,0 @@ ---- -- :left: - :type: match - :number: 6 - :text: "@@ -6,12 +6,18 @@ module Popen" - :line_code: - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: match - :number: 6 - :text: "@@ -6,12 +6,18 @@ module Popen" - :line_code: - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 6 - :text: |2 - <span id="LC6" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 6 - :new_line: 6 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 6 - :text: |2 - <span id="LC6" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 6 - :new_line: 6 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 7 - :text: |2 - <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 7 - :new_line: 7 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 7 - :text: |2 - <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 7 - :new_line: 7 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 8 - :text: |2 - <span id="LC8" class="line"> <span class="k">unless</span> <span class="n">cmd</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Array</span><span class="p">)</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 8 - :new_line: 8 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 8 - :text: |2 - <span id="LC8" class="line"> <span class="k">unless</span> <span class="n">cmd</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Array</span><span class="p">)</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 8 - :new_line: 8 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: old - :number: 9 - :text: | - -<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 9 - :new_line: - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 9 - :text: | - +<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 9 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 10 - :text: |2 - <span id="LC10" class="line"> <span class="k">end</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 10 - :new_line: 10 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 10 - :text: |2 - <span id="LC10" class="line"> <span class="k">end</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 10 - :new_line: 10 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 11 - :text: |2 - <span id="LC11" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 11 - :new_line: 11 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 11 - :text: |2 - <span id="LC11" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 11 - :new_line: 11 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 12 - :text: |2 - <span id="LC12" class="line"> <span class="n">path</span> <span class="o">||=</span> <span class="no">Dir</span><span class="p">.</span><span class="nf">pwd</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 12 - :new_line: 12 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 12 - :text: |2 - <span id="LC12" class="line"> <span class="n">path</span> <span class="o">||=</span> <span class="no">Dir</span><span class="p">.</span><span class="nf">pwd</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 12 - :new_line: 12 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: old - :number: 13 - :text: | - -<span id="LC13" class="line"> <span class="n">vars</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">"PWD"</span> <span class="o">=></span> <span class="n">path</span> <span class="p">}</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 13 - :new_line: - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 13 - :text: | - +<span id="LC13" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_13 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 13 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: old - :number: 14 - :text: | - -<span id="LC14" class="line"> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">chdir: </span><span class="n">path</span> <span class="p">}</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_13 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 14 - :new_line: - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 14 - :text: | - +<span id="LC14" class="line"> <span class="n">vars</span> <span class="o">=</span> <span class="p">{</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 14 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: - :text: '' - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 15 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 15 - :text: | - +<span id="LC15" class="line"> <span class="s2">"PWD"</span> <span class="o">=></span> <span class="n">path</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 15 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: - :text: '' - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 16 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 16 - :text: | - +<span id="LC16" class="line"> <span class="p">}</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 16 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: - :text: '' - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 17 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 17 - :text: | - +<span id="LC17" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 17 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: - :text: '' - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 18 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 18 - :text: | - +<span id="LC18" class="line"> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 18 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: - :text: '' - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 19 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 19 - :text: | - +<span id="LC19" class="line"> <span class="ss">chdir: </span><span class="n">path</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 19 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: - :text: '' - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 20 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 20 - :text: | - +<span id="LC20" class="line"> <span class="p">}</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 20 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 15 - :text: |2 - <span id="LC21" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 15 - :new_line: 21 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 21 - :text: |2 - <span id="LC21" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 15 - :new_line: 21 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 16 - :text: |2 - <span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 16 - :new_line: 22 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 22 - :text: |2 - <span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 16 - :new_line: 22 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 17 - :text: |2 - <span id="LC23" class="line"> <span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 17 - :new_line: 23 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 23 - :text: |2 - <span id="LC23" class="line"> <span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 17 - :new_line: 23 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: match - :number: 19 - :text: "@@ -19,6 +25,7 @@ module Popen" - :line_code: - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: match - :number: 25 - :text: "@@ -19,6 +25,7 @@ module Popen" - :line_code: - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 19 - :text: |2 - <span id="LC25" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 19 - :new_line: 25 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 25 - :text: |2 - <span id="LC25" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 19 - :new_line: 25 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 20 - :text: |2 - <span id="LC26" class="line"> <span class="vi">@cmd_output</span> <span class="o">=</span> <span class="s2">""</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 20 - :new_line: 26 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 26 - :text: |2 - <span id="LC26" class="line"> <span class="vi">@cmd_output</span> <span class="o">=</span> <span class="s2">""</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 20 - :new_line: 26 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 21 - :text: |2 - <span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 21 - :new_line: 27 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 27 - :text: |2 - <span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 21 - :new_line: 27 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: - :text: '' - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 28 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 28 - :text: | - +<span id="LC28" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 28 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 22 - :text: |2 - <span id="LC29" class="line"> <span class="no">Open3</span><span class="p">.</span><span class="nf">popen3</span><span class="p">(</span><span class="n">vars</span><span class="p">,</span> <span class="o">*</span><span class="n">cmd</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">stdin</span><span class="p">,</span> <span class="n">stdout</span><span class="p">,</span> <span class="n">stderr</span><span class="p">,</span> <span class="n">wait_thr</span><span class="o">|</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 22 - :new_line: 29 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 29 - :text: |2 - <span id="LC29" class="line"> <span class="no">Open3</span><span class="p">.</span><span class="nf">popen3</span><span class="p">(</span><span class="n">vars</span><span class="p">,</span> <span class="o">*</span><span class="n">cmd</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">stdin</span><span class="p">,</span> <span class="n">stdout</span><span class="p">,</span> <span class="n">stderr</span><span class="p">,</span> <span class="n">wait_thr</span><span class="o">|</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 22 - :new_line: 29 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 23 - :text: |2 - <span id="LC30" class="line"> <span class="vi">@cmd_output</span> <span class="o"><<</span> <span class="n">stdout</span><span class="p">.</span><span class="nf">read</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 23 - :new_line: 30 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 30 - :text: |2 - <span id="LC30" class="line"> <span class="vi">@cmd_output</span> <span class="o"><<</span> <span class="n">stdout</span><span class="p">.</span><span class="nf">read</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 23 - :new_line: 30 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 24 - :text: |2 - <span id="LC31" class="line"> <span class="vi">@cmd_output</span> <span class="o"><<</span> <span class="n">stderr</span><span class="p">.</span><span class="nf">read</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 24 - :new_line: 31 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 31 - :text: |2 - <span id="LC31" class="line"> <span class="vi">@cmd_output</span> <span class="o"><<</span> <span class="n">stderr</span><span class="p">.</span><span class="nf">read</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 24 - :new_line: 31 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d diff --git a/spec/fixtures/video_sample.mp4 b/spec/fixtures/video_sample.mp4 Binary files differnew file mode 100644 index 00000000000..acd45190998 --- /dev/null +++ b/spec/fixtures/video_sample.mp4 diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb index 08a93503258..af371248ae9 100644 --- a/spec/helpers/notes_helper_spec.rb +++ b/spec/helpers/notes_helper_spec.rb @@ -1,37 +1,30 @@ require "spec_helper" describe NotesHelper do - describe "#notes_max_access_for_users" do - let(:owner) { create(:owner) } - let(:group) { create(:group) } - let(:project) { create(:empty_project, namespace: group) } - let(:master) { create(:user) } - let(:reporter) { create(:user) } - let(:guest) { create(:user) } - - let(:owner_note) { create(:note, author: owner, project: project) } - let(:master_note) { create(:note, author: master, project: project) } - let(:reporter_note) { create(:note, author: reporter, project: project) } - let!(:notes) { [owner_note, master_note, reporter_note] } - - before do - group.add_owner(owner) - project.team << [master, :master] - project.team << [reporter, :reporter] - project.team << [guest, :guest] - end + let(:owner) { create(:owner) } + let(:group) { create(:group) } + let(:project) { create(:empty_project, namespace: group) } + let(:master) { create(:user) } + let(:reporter) { create(:user) } + let(:guest) { create(:user) } - it 'return human access levels' do - original_method = project.team.method(:human_max_access) - expect_any_instance_of(ProjectTeam).to receive(:human_max_access).exactly(3).times do |*args| - original_method.call(args[1]) - end + let(:owner_note) { create(:note, author: owner, project: project) } + let(:master_note) { create(:note, author: master, project: project) } + let(:reporter_note) { create(:note, author: reporter, project: project) } + let!(:notes) { [owner_note, master_note, reporter_note] } + before do + group.add_owner(owner) + project.team << [master, :master] + project.team << [reporter, :reporter] + project.team << [guest, :guest] + end + + describe "#notes_max_access_for_users" do + it 'return human access levels' do expect(helper.note_max_access_for_user(owner_note)).to eq('Owner') expect(helper.note_max_access_for_user(master_note)).to eq('Master') expect(helper.note_max_access_for_user(reporter_note)).to eq('Reporter') - # Call it again to ensure value is cached - expect(helper.note_max_access_for_user(owner_note)).to eq('Owner') end it 'handles access in different projects' do @@ -43,4 +36,16 @@ describe NotesHelper do expect(helper.note_max_access_for_user(other_note)).to eq('Reporter') end end + + describe '#preload_max_access_for_authors' do + it 'loads multiple users' do + expected_access = { + owner.id => Gitlab::Access::OWNER, + master.id => Gitlab::Access::MASTER, + reporter.id => Gitlab::Access::REPORTER + } + + expect(helper.preload_max_access_for_authors(notes, project)).to eq(expected_access) + end + end end diff --git a/spec/helpers/time_helper_spec.rb b/spec/helpers/time_helper_spec.rb index 413ead944b9..bf3ed5c094c 100644 --- a/spec/helpers/time_helper_spec.rb +++ b/spec/helpers/time_helper_spec.rb @@ -5,6 +5,7 @@ describe TimeHelper do it "returns minutes and seconds" do intervals_in_words = { 100 => "1 minute 40 seconds", + 100.32 => "1 minute 40 seconds", 121 => "2 minutes 1 second", 3721 => "62 minutes 1 second", 0 => "0 seconds" diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb index 5178bd130f4..baab30f482f 100644 --- a/spec/initializers/6_validations_spec.rb +++ b/spec/initializers/6_validations_spec.rb @@ -1,41 +1,58 @@ require 'spec_helper' +require_relative '../../config/initializers/6_validations.rb' describe '6_validations', lib: true do + before :all do + FileUtils.mkdir_p('tmp/tests/paths/a/b/c/d') + FileUtils.mkdir_p('tmp/tests/paths/a/b/c2') + FileUtils.mkdir_p('tmp/tests/paths/a/b/d') + end + + after :all do + FileUtils.rm_rf('tmp/tests/paths') + end + context 'with correct settings' do before do - mock_storages('foo' => '/a/b/c', 'bar' => 'a/b/d') + mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/d') end it 'passes through' do - expect { load_validations }.not_to raise_error + expect { validate_storages }.not_to raise_error end end context 'with invalid storage names' do before do - mock_storages('name with spaces' => '/a/b/c') + mock_storages('name with spaces' => 'tmp/tests/paths/a/b/c') end it 'throws an error' do - expect { load_validations }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.') + expect { validate_storages }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.') end end context 'with nested storage paths' do before do - mock_storages('foo' => '/a/b/c', 'bar' => '/a/b/c/d') + mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/c/d') end it 'throws an error' do - expect { load_validations }.to raise_error('bar is a nested path of foo. Nested paths are not supported for repository storages. Please fix this in your gitlab.yml before starting GitLab.') + expect { validate_storages }.to raise_error('bar is a nested path of foo. Nested paths are not supported for repository storages. Please fix this in your gitlab.yml before starting GitLab.') end end - def mock_storages(storages) - allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + context 'with similar but un-nested storage paths' do + before do + mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/c2') + end + + it 'passes through' do + expect { validate_storages }.not_to raise_error + end end - def load_validations - load File.join(__dir__, '../../config/initializers/6_validations.rb') + def mock_storages(storages) + allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) end end diff --git a/spec/initializers/trusted_proxies_spec.rb b/spec/initializers/trusted_proxies_spec.rb index 14c8df954a6..52d5a7dffc9 100644 --- a/spec/initializers/trusted_proxies_spec.rb +++ b/spec/initializers/trusted_proxies_spec.rb @@ -17,6 +17,12 @@ describe 'trusted_proxies', lib: true do expect(request.remote_ip).to eq('10.1.5.89') expect(request.ip).to eq('10.1.5.89') end + + it 'filters out bad values' do + request = stub_request('HTTP_X_FORWARDED_FOR' => '(null), 10.1.5.89') + expect(request.remote_ip).to eq('10.1.5.89') + expect(request.ip).to eq('10.1.5.89') + end end context 'with private IP ranges added' do diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js new file mode 100644 index 00000000000..b48026c3b77 --- /dev/null +++ b/spec/javascripts/application_spec.js @@ -0,0 +1,32 @@ + +/*= require lib/utils/common_utils */ + +(function() { + describe('Application', function() { + return describe('disable buttons', function() { + fixture.preload('application.html'); + beforeEach(function() { + return fixture.load('application.html'); + }); + it('should prevent default action for disabled buttons', function() { + var $button, isClicked; + gl.utils.preventDisabledButtons(); + isClicked = false; + $button = $('#test-button'); + $button.click(function() { + return isClicked = true; + }); + $button.trigger('click'); + return expect(isClicked).toBe(false); + }); + return it('should be on the same page if a disabled link clicked', function() { + var locationBeforeLinkClick; + locationBeforeLinkClick = window.location.href; + gl.utils.preventDisabledButtons(); + $('#test-link').click(); + return expect(window.location.href).toBe(locationBeforeLinkClick); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/application_spec.js.coffee b/spec/javascripts/application_spec.js.coffee deleted file mode 100644 index 4b6a2bb5440..00000000000 --- a/spec/javascripts/application_spec.js.coffee +++ /dev/null @@ -1,30 +0,0 @@ -#= require lib/utils/common_utils - -describe 'Application', -> - describe 'disable buttons', -> - fixture.preload('application.html') - - beforeEach -> - fixture.load('application.html') - - it 'should prevent default action for disabled buttons', -> - - gl.utils.preventDisabledButtons() - - isClicked = false - $button = $ '#test-button' - - $button.click -> isClicked = true - $button.trigger 'click' - - expect(isClicked).toBe false - - - it 'should be on the same page if a disabled link clicked', -> - - locationBeforeLinkClick = window.location.href - gl.utils.preventDisabledButtons() - - $('#test-link').click() - - expect(window.location.href).toBe locationBeforeLinkClick diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js new file mode 100644 index 00000000000..3ddc163033e --- /dev/null +++ b/spec/javascripts/awards_handler_spec.js @@ -0,0 +1,187 @@ + +/*= require awards_handler */ + + +/*= require jquery */ + + +/*= require jquery.cookie */ + + +/*= require ./fixtures/emoji_menu */ + +(function() { + var awardsHandler, lazyAssert; + + awardsHandler = null; + + window.gl || (window.gl = {}); + + window.gon || (window.gon = {}); + + gl.emojiAliases = function() { + return { + '+1': 'thumbsup', + '-1': 'thumbsdown' + }; + }; + + gon.award_menu_url = '/emojis'; + + lazyAssert = function(done, assertFn) { + return setTimeout(function() { + assertFn(); + return done(); + }, 333); + }; + + describe('AwardsHandler', function() { + fixture.preload('awards_handler.html'); + beforeEach(function() { + fixture.load('awards_handler.html'); + awardsHandler = new AwardsHandler; + spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) { + return function(url, emoji, cb) { + return cb(); + }; + })(this)); + return spyOn(jQuery, 'get').and.callFake(function(req, cb) { + return cb(window.emojiMenu); + }); + }); + describe('::showEmojiMenu', function() { + it('should show emoji menu when Add emoji button clicked', function(done) { + $('.js-add-award').eq(0).click(); + return lazyAssert(done, function() { + var $emojiMenu; + $emojiMenu = $('.emoji-menu'); + expect($emojiMenu.length).toBe(1); + expect($emojiMenu.hasClass('is-visible')).toBe(true); + expect($emojiMenu.find('#emoji_search').length).toBe(1); + return expect($('.js-awards-block.current').length).toBe(1); + }); + }); + it('should also show emoji menu for the smiley icon in notes', function(done) { + $('.note-action-button').click(); + return lazyAssert(done, function() { + var $emojiMenu; + $emojiMenu = $('.emoji-menu'); + return expect($emojiMenu.length).toBe(1); + }); + }); + return it('should remove emoji menu when body is clicked', function(done) { + $('.js-add-award').eq(0).click(); + return lazyAssert(done, function() { + var $emojiMenu; + $emojiMenu = $('.emoji-menu'); + $('body').click(); + expect($emojiMenu.length).toBe(1); + expect($emojiMenu.hasClass('is-visible')).toBe(false); + return expect($('.js-awards-block.current').length).toBe(0); + }); + }); + }); + describe('::addAwardToEmojiBar', function() { + it('should add emoji to votes block', function() { + var $emojiButton, $votesBlock; + $votesBlock = $('.js-awards-block').eq(0); + awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); + $emojiButton = $votesBlock.find('[data-emoji=heart]'); + expect($emojiButton.length).toBe(1); + expect($emojiButton.next('.js-counter').text()).toBe('1'); + return expect($votesBlock.hasClass('hidden')).toBe(false); + }); + it('should remove the emoji when we click again', function() { + var $emojiButton, $votesBlock; + $votesBlock = $('.js-awards-block').eq(0); + awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); + awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); + $emojiButton = $votesBlock.find('[data-emoji=heart]'); + return expect($emojiButton.length).toBe(0); + }); + return it('should decrement the emoji counter', function() { + var $emojiButton, $votesBlock; + $votesBlock = $('.js-awards-block').eq(0); + awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); + $emojiButton = $votesBlock.find('[data-emoji=heart]'); + $emojiButton.next('.js-counter').text(5); + awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); + expect($emojiButton.length).toBe(1); + return expect($emojiButton.next('.js-counter').text()).toBe('4'); + }); + }); + describe('::getAwardUrl', function() { + return it('should return the url for request', function() { + return expect(awardsHandler.getAwardUrl()).toBe('/gitlab-org/gitlab-test/issues/8/toggle_award_emoji'); + }); + }); + describe('::addAward and ::checkMutuality', function() { + return it('should handle :+1: and :-1: mutuality', function() { + var $thumbsDownEmoji, $thumbsUpEmoji, $votesBlock, awardUrl; + awardUrl = awardsHandler.getAwardUrl(); + $votesBlock = $('.js-awards-block').eq(0); + $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent(); + $thumbsDownEmoji = $votesBlock.find('[data-emoji=thumbsdown]').parent(); + awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); + expect($thumbsUpEmoji.hasClass('active')).toBe(true); + expect($thumbsDownEmoji.hasClass('active')).toBe(false); + $thumbsUpEmoji.tooltip(); + $thumbsDownEmoji.tooltip(); + awardsHandler.addAward($votesBlock, awardUrl, 'thumbsdown', true); + expect($thumbsUpEmoji.hasClass('active')).toBe(false); + return expect($thumbsDownEmoji.hasClass('active')).toBe(true); + }); + }); + describe('::removeEmoji', function() { + return it('should remove emoji', function() { + var $votesBlock, awardUrl; + awardUrl = awardsHandler.getAwardUrl(); + $votesBlock = $('.js-awards-block').eq(0); + awardsHandler.addAward($votesBlock, awardUrl, 'fire', false); + expect($votesBlock.find('[data-emoji=fire]').length).toBe(1); + awardsHandler.removeEmoji($votesBlock.find('[data-emoji=fire]').closest('button')); + return expect($votesBlock.find('[data-emoji=fire]').length).toBe(0); + }); + }); + describe('search', function() { + return it('should filter the emoji', function() { + $('.js-add-award').eq(0).click(); + expect($('[data-emoji=angel]').is(':visible')).toBe(true); + expect($('[data-emoji=anger]').is(':visible')).toBe(true); + $('#emoji_search').val('ali').trigger('keyup'); + expect($('[data-emoji=angel]').is(':visible')).toBe(false); + expect($('[data-emoji=anger]').is(':visible')).toBe(false); + return expect($('[data-emoji=alien]').is(':visible')).toBe(true); + }); + }); + return describe('emoji menu', function() { + var openEmojiMenuAndAddEmoji, selector; + selector = '[data-emoji=sunglasses]'; + openEmojiMenuAndAddEmoji = function() { + var $block, $emoji, $menu; + $('.js-add-award').eq(0).click(); + $menu = $('.emoji-menu'); + $block = $('.js-awards-block'); + $emoji = $menu.find(".emoji-menu-list-item " + selector); + expect($emoji.length).toBe(1); + expect($block.find(selector).length).toBe(0); + $emoji.click(); + expect($menu.hasClass('.is-visible')).toBe(false); + return expect($block.find(selector).length).toBe(1); + }; + it('should add selected emoji to awards block', function() { + return openEmojiMenuAndAddEmoji(); + }); + return it('should remove already selected emoji', function() { + var $block, $emoji; + openEmojiMenuAndAddEmoji(); + $('.js-add-award').eq(0).click(); + $block = $('.js-awards-block'); + $emoji = $('.emoji-menu').find(".emoji-menu-list-item " + selector); + $emoji.click(); + return expect($block.find(selector).length).toBe(0); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/awards_handler_spec.js.coffee b/spec/javascripts/awards_handler_spec.js.coffee deleted file mode 100644 index d7f9c6fc076..00000000000 --- a/spec/javascripts/awards_handler_spec.js.coffee +++ /dev/null @@ -1,200 +0,0 @@ -#= require awards_handler -#= require jquery -#= require jquery.cookie -#= require ./fixtures/emoji_menu - -awardsHandler = null -window.gl or= {} -window.gon or= {} -gl.emojiAliases = -> return { '+1': 'thumbsup', '-1': 'thumbsdown' } -gon.award_menu_url = '/emojis' - - -lazyAssert = (done, assertFn) -> - - setTimeout -> # Maybe jasmine.clock here? - assertFn() - done() - , 333 - - -describe 'AwardsHandler', -> - - fixture.preload 'awards_handler.html' - - beforeEach -> - fixture.load 'awards_handler.html' - awardsHandler = new AwardsHandler - spyOn(awardsHandler, 'postEmoji').and.callFake (url, emoji, cb) => cb() - spyOn(jQuery, 'get').and.callFake (req, cb) -> cb window.emojiMenu - - - describe '::showEmojiMenu', -> - - it 'should show emoji menu when Add emoji button clicked', (done) -> - - $('.js-add-award').eq(0).click() - - lazyAssert done, -> - $emojiMenu = $ '.emoji-menu' - expect($emojiMenu.length).toBe 1 - expect($emojiMenu.hasClass('is-visible')).toBe yes - expect($emojiMenu.find('#emoji_search').length).toBe 1 - expect($('.js-awards-block.current').length).toBe 1 - - - it 'should also show emoji menu for the smiley icon in notes', (done) -> - - $('.note-action-button').click() - - lazyAssert done, -> - $emojiMenu = $ '.emoji-menu' - expect($emojiMenu.length).toBe 1 - - - it 'should remove emoji menu when body is clicked', (done) -> - - $('.js-add-award').eq(0).click() - - lazyAssert done, -> - $emojiMenu = $('.emoji-menu') - $('body').click() - expect($emojiMenu.length).toBe 1 - expect($emojiMenu.hasClass('is-visible')).toBe no - expect($('.js-awards-block.current').length).toBe 0 - - - describe '::addAwardToEmojiBar', -> - - it 'should add emoji to votes block', -> - - $votesBlock = $('.js-awards-block').eq 0 - awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no - - $emojiButton = $votesBlock.find '[data-emoji=heart]' - - expect($emojiButton.length).toBe 1 - expect($emojiButton.next('.js-counter').text()).toBe '1' - expect($votesBlock.hasClass('hidden')).toBe no - - - it 'should remove the emoji when we click again', -> - - $votesBlock = $('.js-awards-block').eq 0 - awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no - awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no - $emojiButton = $votesBlock.find '[data-emoji=heart]' - - expect($emojiButton.length).toBe 0 - - - it 'should decrement the emoji counter', -> - - $votesBlock = $('.js-awards-block').eq 0 - awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no - - $emojiButton = $votesBlock.find '[data-emoji=heart]' - $emojiButton.next('.js-counter').text 5 - - awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no - - expect($emojiButton.length).toBe 1 - expect($emojiButton.next('.js-counter').text()).toBe '4' - - - describe '::getAwardUrl', -> - - it 'should return the url for request', -> - - expect(awardsHandler.getAwardUrl()).toBe '/gitlab-org/gitlab-test/issues/8/toggle_award_emoji' - - - describe '::addAward and ::checkMutuality', -> - - it 'should handle :+1: and :-1: mutuality', -> - - awardUrl = awardsHandler.getAwardUrl() - $votesBlock = $('.js-awards-block').eq 0 - $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent() - $thumbsDownEmoji = $votesBlock.find('[data-emoji=thumbsdown]').parent() - - awardsHandler.addAward $votesBlock, awardUrl, 'thumbsup', no - - expect($thumbsUpEmoji.hasClass('active')).toBe yes - expect($thumbsDownEmoji.hasClass('active')).toBe no - - $thumbsUpEmoji.tooltip() - $thumbsDownEmoji.tooltip() - - awardsHandler.addAward $votesBlock, awardUrl, 'thumbsdown', yes - - expect($thumbsUpEmoji.hasClass('active')).toBe no - expect($thumbsDownEmoji.hasClass('active')).toBe yes - - - describe '::removeEmoji', -> - - it 'should remove emoji', -> - - awardUrl = awardsHandler.getAwardUrl() - $votesBlock = $('.js-awards-block').eq 0 - - awardsHandler.addAward $votesBlock, awardUrl, 'fire', no - expect($votesBlock.find('[data-emoji=fire]').length).toBe 1 - - awardsHandler.removeEmoji $votesBlock.find('[data-emoji=fire]').closest('button') - expect($votesBlock.find('[data-emoji=fire]').length).toBe 0 - - - describe 'search', -> - - it 'should filter the emoji', -> - - $('.js-add-award').eq(0).click() - - expect($('[data-emoji=angel]').is(':visible')).toBe yes - expect($('[data-emoji=anger]').is(':visible')).toBe yes - - $('#emoji_search').val('ali').trigger 'keyup' - - expect($('[data-emoji=angel]').is(':visible')).toBe no - expect($('[data-emoji=anger]').is(':visible')).toBe no - expect($('[data-emoji=alien]').is(':visible')).toBe yes - - - describe 'emoji menu', -> - - selector = '[data-emoji=sunglasses]' - - openEmojiMenuAndAddEmoji = -> - - $('.js-add-award').eq(0).click() - - $menu = $ '.emoji-menu' - $block = $ '.js-awards-block' - $emoji = $menu.find ".emoji-menu-list-item #{selector}" - - expect($emoji.length).toBe 1 - expect($block.find(selector).length).toBe 0 - - $emoji.click() - - expect($menu.hasClass('.is-visible')).toBe no - expect($block.find(selector).length).toBe 1 - - - it 'should add selected emoji to awards block', -> - - openEmojiMenuAndAddEmoji() - - - it 'should remove already selected emoji', -> - - openEmojiMenuAndAddEmoji() - $('.js-add-award').eq(0).click() - - $block = $ '.js-awards-block' - $emoji = $('.emoji-menu').find ".emoji-menu-list-item #{selector}" - - $emoji.click() - expect($block.find(selector).length).toBe 0 diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js new file mode 100644 index 00000000000..78795f7654a --- /dev/null +++ b/spec/javascripts/behaviors/autosize_spec.js @@ -0,0 +1,21 @@ + +/*= require behaviors/autosize */ + +(function() { + describe('Autosize behavior', function() { + var load; + beforeEach(function() { + return fixture.set('<textarea class="js-autosize" style="resize: vertical"></textarea>'); + }); + it('does not overwrite the resize property', function() { + load(); + return expect($('textarea')).toHaveCss({ + resize: 'vertical' + }); + }); + return load = function() { + return $(document).trigger('page:load'); + }; + }); + +}).call(this); diff --git a/spec/javascripts/behaviors/autosize_spec.js.coffee b/spec/javascripts/behaviors/autosize_spec.js.coffee deleted file mode 100644 index 7fc1d19c35f..00000000000 --- a/spec/javascripts/behaviors/autosize_spec.js.coffee +++ /dev/null @@ -1,11 +0,0 @@ -#= require behaviors/autosize - -describe 'Autosize behavior', -> - beforeEach -> - fixture.set('<textarea class="js-autosize" style="resize: vertical"></textarea>') - - it 'does not overwrite the resize property', -> - load() - expect($('textarea')).toHaveCss(resize: 'vertical') - - load = -> $(document).trigger('page:load') diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js new file mode 100644 index 00000000000..4c52ecd903d --- /dev/null +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -0,0 +1,93 @@ + +/*= require behaviors/quick_submit */ + +(function() { + describe('Quick Submit behavior', function() { + var keydownEvent; + fixture.preload('behaviors/quick_submit.html'); + beforeEach(function() { + fixture.load('behaviors/quick_submit.html'); + $('form').submit(function(e) { + return e.preventDefault(); + }); + return this.spies = { + submit: spyOnEvent('form', 'submit') + }; + }); + it('does not respond to other keyCodes', function() { + $('input.quick-submit-input').trigger(keydownEvent({ + keyCode: 32 + })); + return expect(this.spies.submit).not.toHaveBeenTriggered(); + }); + it('does not respond to Enter alone', function() { + $('input.quick-submit-input').trigger(keydownEvent({ + ctrlKey: false, + metaKey: false + })); + return expect(this.spies.submit).not.toHaveBeenTriggered(); + }); + it('does not respond to repeated events', function() { + $('input.quick-submit-input').trigger(keydownEvent({ + repeat: true + })); + return expect(this.spies.submit).not.toHaveBeenTriggered(); + }); + it('disables submit buttons', function() { + $('textarea').trigger(keydownEvent()); + expect($('input[type=submit]')).toBeDisabled(); + return expect($('button[type=submit]')).toBeDisabled(); + }); + if (navigator.userAgent.match(/Macintosh/)) { + it('responds to Meta+Enter', function() { + $('input.quick-submit-input').trigger(keydownEvent()); + return expect(this.spies.submit).toHaveBeenTriggered(); + }); + it('excludes other modifier keys', function() { + $('input.quick-submit-input').trigger(keydownEvent({ + altKey: true + })); + $('input.quick-submit-input').trigger(keydownEvent({ + ctrlKey: true + })); + $('input.quick-submit-input').trigger(keydownEvent({ + shiftKey: true + })); + return expect(this.spies.submit).not.toHaveBeenTriggered(); + }); + } else { + it('responds to Ctrl+Enter', function() { + $('input.quick-submit-input').trigger(keydownEvent()); + return expect(this.spies.submit).toHaveBeenTriggered(); + }); + it('excludes other modifier keys', function() { + $('input.quick-submit-input').trigger(keydownEvent({ + altKey: true + })); + $('input.quick-submit-input').trigger(keydownEvent({ + metaKey: true + })); + $('input.quick-submit-input').trigger(keydownEvent({ + shiftKey: true + })); + return expect(this.spies.submit).not.toHaveBeenTriggered(); + }); + } + return keydownEvent = function(options) { + var defaults; + if (navigator.userAgent.match(/Macintosh/)) { + defaults = { + keyCode: 13, + metaKey: true + }; + } else { + defaults = { + keyCode: 13, + ctrlKey: true + }; + } + return $.Event('keydown', $.extend({}, defaults, options)); + }; + }); + +}).call(this); diff --git a/spec/javascripts/behaviors/quick_submit_spec.js.coffee b/spec/javascripts/behaviors/quick_submit_spec.js.coffee deleted file mode 100644 index d3b003a328a..00000000000 --- a/spec/javascripts/behaviors/quick_submit_spec.js.coffee +++ /dev/null @@ -1,70 +0,0 @@ -#= require behaviors/quick_submit - -describe 'Quick Submit behavior', -> - fixture.preload('behaviors/quick_submit.html') - - beforeEach -> - fixture.load('behaviors/quick_submit.html') - - # Prevent a form submit from moving us off the testing page - $('form').submit (e) -> e.preventDefault() - - @spies = { - submit: spyOnEvent('form', 'submit') - } - - it 'does not respond to other keyCodes', -> - $('input.quick-submit-input').trigger(keydownEvent(keyCode: 32)) - - expect(@spies.submit).not.toHaveBeenTriggered() - - it 'does not respond to Enter alone', -> - $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: false, metaKey: false)) - - expect(@spies.submit).not.toHaveBeenTriggered() - - it 'does not respond to repeated events', -> - $('input.quick-submit-input').trigger(keydownEvent(repeat: true)) - - expect(@spies.submit).not.toHaveBeenTriggered() - - it 'disables submit buttons', -> - $('textarea').trigger(keydownEvent()) - - expect($('input[type=submit]')).toBeDisabled() - expect($('button[type=submit]')).toBeDisabled() - - # We cannot stub `navigator.userAgent` for CI's `rake teaspoon` task, so we'll - # only run the tests that apply to the current platform - if navigator.userAgent.match(/Macintosh/) - it 'responds to Meta+Enter', -> - $('input.quick-submit-input').trigger(keydownEvent()) - - expect(@spies.submit).toHaveBeenTriggered() - - it 'excludes other modifier keys', -> - $('input.quick-submit-input').trigger(keydownEvent(altKey: true)) - $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: true)) - $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true)) - - expect(@spies.submit).not.toHaveBeenTriggered() - else - it 'responds to Ctrl+Enter', -> - $('input.quick-submit-input').trigger(keydownEvent()) - - expect(@spies.submit).toHaveBeenTriggered() - - it 'excludes other modifier keys', -> - $('input.quick-submit-input').trigger(keydownEvent(altKey: true)) - $('input.quick-submit-input').trigger(keydownEvent(metaKey: true)) - $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true)) - - expect(@spies.submit).not.toHaveBeenTriggered() - - keydownEvent = (options) -> - if navigator.userAgent.match(/Macintosh/) - defaults = { keyCode: 13, metaKey: true } - else - defaults = { keyCode: 13, ctrlKey: true } - - $.Event('keydown', $.extend({}, defaults, options)) diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js new file mode 100644 index 00000000000..724c3baf989 --- /dev/null +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -0,0 +1,44 @@ + +/*= require behaviors/requires_input */ + +(function() { + describe('requiresInput', function() { + fixture.preload('behaviors/requires_input.html'); + beforeEach(function() { + return fixture.load('behaviors/requires_input.html'); + }); + it('disables submit when any field is required', function() { + $('.js-requires-input').requiresInput(); + return expect($('.submit')).toBeDisabled(); + }); + it('enables submit when no field is required', function() { + $('*[required=required]').removeAttr('required'); + $('.js-requires-input').requiresInput(); + return expect($('.submit')).not.toBeDisabled(); + }); + it('enables submit when all required fields are pre-filled', function() { + $('*[required=required]').remove(); + $('.js-requires-input').requiresInput(); + return expect($('.submit')).not.toBeDisabled(); + }); + it('enables submit when all required fields receive input', function() { + $('.js-requires-input').requiresInput(); + $('#required1').val('input1').change(); + expect($('.submit')).toBeDisabled(); + $('#optional1').val('input1').change(); + expect($('.submit')).toBeDisabled(); + $('#required2').val('input2').change(); + $('#required3').val('input3').change(); + $('#required4').val('input4').change(); + $('#required5').val('1').change(); + return expect($('.submit')).not.toBeDisabled(); + }); + return it('is called on page:load event', function() { + var spy; + spy = spyOn($.fn, 'requiresInput'); + $(document).trigger('page:load'); + return expect(spy).toHaveBeenCalled(); + }); + }); + +}).call(this); diff --git a/spec/javascripts/behaviors/requires_input_spec.js.coffee b/spec/javascripts/behaviors/requires_input_spec.js.coffee deleted file mode 100644 index 61a17632173..00000000000 --- a/spec/javascripts/behaviors/requires_input_spec.js.coffee +++ /dev/null @@ -1,49 +0,0 @@ -#= require behaviors/requires_input - -describe 'requiresInput', -> - fixture.preload('behaviors/requires_input.html') - - beforeEach -> - fixture.load('behaviors/requires_input.html') - - it 'disables submit when any field is required', -> - $('.js-requires-input').requiresInput() - - expect($('.submit')).toBeDisabled() - - it 'enables submit when no field is required', -> - $('*[required=required]').removeAttr('required') - - $('.js-requires-input').requiresInput() - - expect($('.submit')).not.toBeDisabled() - - it 'enables submit when all required fields are pre-filled', -> - $('*[required=required]').remove() - - $('.js-requires-input').requiresInput() - - expect($('.submit')).not.toBeDisabled() - - it 'enables submit when all required fields receive input', -> - $('.js-requires-input').requiresInput() - - $('#required1').val('input1').change() - expect($('.submit')).toBeDisabled() - - $('#optional1').val('input1').change() - expect($('.submit')).toBeDisabled() - - $('#required2').val('input2').change() - $('#required3').val('input3').change() - $('#required4').val('input4').change() - $('#required5').val('1').change() - - expect($('.submit')).not.toBeDisabled() - - it 'is called on page:load event', -> - spy = spyOn($.fn, 'requiresInput') - - $(document).trigger('page:load') - - expect(spy).toHaveBeenCalled() diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js new file mode 100644 index 00000000000..eced2f6575d --- /dev/null +++ b/spec/javascripts/extensions/array_spec.js @@ -0,0 +1,22 @@ + +/*= require extensions/array */ + +(function() { + describe('Array extensions', function() { + describe('first', function() { + return it('returns the first item', function() { + var arr; + arr = [0, 1, 2, 3, 4, 5]; + return expect(arr.first()).toBe(0); + }); + }); + return describe('last', function() { + return it('returns the last item', function() { + var arr; + arr = [0, 1, 2, 3, 4, 5]; + return expect(arr.last()).toBe(5); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/extensions/array_spec.js.coffee b/spec/javascripts/extensions/array_spec.js.coffee deleted file mode 100644 index 4ceac619422..00000000000 --- a/spec/javascripts/extensions/array_spec.js.coffee +++ /dev/null @@ -1,12 +0,0 @@ -#= require extensions/array - -describe 'Array extensions', -> - describe 'first', -> - it 'returns the first item', -> - arr = [0, 1, 2, 3, 4, 5] - expect(arr.first()).toBe(0) - - describe 'last', -> - it 'returns the last item', -> - arr = [0, 1, 2, 3, 4, 5] - expect(arr.last()).toBe(5) diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js new file mode 100644 index 00000000000..b644344b95a --- /dev/null +++ b/spec/javascripts/extensions/jquery_spec.js @@ -0,0 +1,42 @@ + +/*= require extensions/jquery */ + +(function() { + describe('jQuery extensions', function() { + describe('disable', function() { + beforeEach(function() { + return fixture.set('<input type="text" />'); + }); + it('adds the disabled attribute', function() { + var $input; + $input = $('input').first(); + $input.disable(); + return expect($input).toHaveAttr('disabled', 'disabled'); + }); + return it('adds the disabled class', function() { + var $input; + $input = $('input').first(); + $input.disable(); + return expect($input).toHaveClass('disabled'); + }); + }); + return describe('enable', function() { + beforeEach(function() { + return fixture.set('<input type="text" disabled="disabled" class="disabled" />'); + }); + it('removes the disabled attribute', function() { + var $input; + $input = $('input').first(); + $input.enable(); + return expect($input).not.toHaveAttr('disabled'); + }); + return it('removes the disabled class', function() { + var $input; + $input = $('input').first(); + $input.enable(); + return expect($input).not.toHaveClass('disabled'); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/extensions/jquery_spec.js.coffee b/spec/javascripts/extensions/jquery_spec.js.coffee deleted file mode 100644 index b10e16b7d01..00000000000 --- a/spec/javascripts/extensions/jquery_spec.js.coffee +++ /dev/null @@ -1,34 +0,0 @@ -#= require extensions/jquery - -describe 'jQuery extensions', -> - describe 'disable', -> - beforeEach -> - fixture.set '<input type="text" />' - - it 'adds the disabled attribute', -> - $input = $('input').first() - - $input.disable() - expect($input).toHaveAttr('disabled', 'disabled') - - it 'adds the disabled class', -> - $input = $('input').first() - - $input.disable() - expect($input).toHaveClass('disabled') - - describe 'enable', -> - beforeEach -> - fixture.set '<input type="text" disabled="disabled" class="disabled" />' - - it 'removes the disabled attribute', -> - $input = $('input').first() - - $input.enable() - expect($input).not.toHaveAttr('disabled') - - it 'removes the disabled class', -> - $input = $('input').first() - - $input.enable() - expect($input).not.toHaveClass('disabled') diff --git a/spec/javascripts/fixtures/emoji_menu.coffee b/spec/javascripts/fixtures/emoji_menu.coffee deleted file mode 100644 index ce1a41390d2..00000000000 --- a/spec/javascripts/fixtures/emoji_menu.coffee +++ /dev/null @@ -1,957 +0,0 @@ -window.emojiMenu = """ - <div class='emoji-menu'> - <input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" /> - <div class='emoji-menu-content'> - <h5 class='emoji-menu-title'> - Emoticons - </h5> - <ul class='clearfix emoji-menu-list'> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F47D" title="alien" data-aliases="" data-emoji="alien" data-unicode-name="1F47D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F47C" title="angel" data-aliases="" data-emoji="angel" data-unicode-name="1F47C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A2" title="anger" data-aliases="" data-emoji="anger" data-unicode-name="1F4A2"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F620" title="angry" data-aliases="" data-emoji="angry" data-unicode-name="1F620"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F627" title="anguished" data-aliases="" data-emoji="anguished" data-unicode-name="1F627"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F632" title="astonished" data-aliases="" data-emoji="astonished" data-unicode-name="1F632"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F45F" title="athletic_shoe" data-aliases="" data-emoji="athletic_shoe" data-unicode-name="1F45F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F476" title="baby" data-aliases="" data-emoji="baby" data-unicode-name="1F476"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F459" title="bikini" data-aliases="" data-emoji="bikini" data-unicode-name="1F459"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F499" title="blue_heart" data-aliases="" data-emoji="blue_heart" data-unicode-name="1F499"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F60A" title="blush" data-aliases="" data-emoji="blush" data-unicode-name="1F60A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A5" title="boom" data-aliases="" data-emoji="boom" data-unicode-name="1F4A5"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F462" title="boot" data-aliases="" data-emoji="boot" data-unicode-name="1F462"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F647" title="bow" data-aliases="" data-emoji="bow" data-unicode-name="1F647"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F466" title="boy" data-aliases="" data-emoji="boy" data-unicode-name="1F466"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F470" title="bride_with_veil" data-aliases="" data-emoji="bride_with_veil" data-unicode-name="1F470"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4BC" title="briefcase" data-aliases="" data-emoji="briefcase" data-unicode-name="1F4BC"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F494" title="broken_heart" data-aliases="" data-emoji="broken_heart" data-unicode-name="1F494"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F464" title="bust_in_silhouette" data-aliases="" data-emoji="bust_in_silhouette" data-unicode-name="1F464"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F465" title="busts_in_silhouette" data-aliases="" data-emoji="busts_in_silhouette" data-unicode-name="1F465"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F44F" title="clap" data-aliases="" data-emoji="clap" data-unicode-name="1F44F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F302" title="closed_umbrella" data-aliases="" data-emoji="closed_umbrella" data-unicode-name="1F302"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F630" title="cold_sweat" data-aliases="" data-emoji="cold_sweat" data-unicode-name="1F630"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F616" title="confounded" data-aliases="" data-emoji="confounded" data-unicode-name="1F616"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F615" title="confused" data-aliases="" data-emoji="confused" data-unicode-name="1F615"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F477" title="construction_worker" data-aliases="" data-emoji="construction_worker" data-unicode-name="1F477"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F46E" title="cop" data-aliases="" data-emoji="cop" data-unicode-name="1F46E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F46B" title="couple" data-aliases="" data-emoji="couple" data-unicode-name="1F46B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F491" title="couple_with_heart" data-aliases="" data-emoji="couple_with_heart" data-unicode-name="1F491"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F48F" title="couplekiss" data-aliases="" data-emoji="couplekiss" data-unicode-name="1F48F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F451" title="crown" data-aliases="" data-emoji="crown" data-unicode-name="1F451"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F622" title="cry" data-aliases="" data-emoji="cry" data-unicode-name="1F622"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F63F" title="crying_cat_face" data-aliases="" data-emoji="crying_cat_face" data-unicode-name="1F63F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F498" title="cupid" data-aliases="" data-emoji="cupid" data-unicode-name="1F498"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F483" title="dancer" data-aliases="" data-emoji="dancer" data-unicode-name="1F483"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F46F" title="dancers" data-aliases="" data-emoji="dancers" data-unicode-name="1F46F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A8" title="dash" data-aliases="" data-emoji="dash" data-unicode-name="1F4A8"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F61E" title="disappointed" data-aliases="" data-emoji="disappointed" data-unicode-name="1F61E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F625" title="disappointed_relieved" data-aliases="" data-emoji="disappointed_relieved" data-unicode-name="1F625"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4AB" title="dizzy" data-aliases="" data-emoji="dizzy" data-unicode-name="1F4AB"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F635" title="dizzy_face" data-aliases="" data-emoji="dizzy_face" data-unicode-name="1F635"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F457" title="dress" data-aliases="" data-emoji="dress" data-unicode-name="1F457"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A7" title="droplet" data-aliases="" data-emoji="droplet" data-unicode-name="1F4A7"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F442" title="ear" data-aliases="" data-emoji="ear" data-unicode-name="1F442"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F611" title="expressionless" data-aliases="" data-emoji="expressionless" data-unicode-name="1F611"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F453" title="eyeglasses" data-aliases="" data-emoji="eyeglasses" data-unicode-name="1F453"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F440" title="eyes" data-aliases="" data-emoji="eyes" data-unicode-name="1F440"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F46A" title="family" data-aliases="" data-emoji="family" data-unicode-name="1F46A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F628" title="fearful" data-aliases="" data-emoji="fearful" data-unicode-name="1F628"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F525" title="fire" data-aliases=":flame:" data-emoji="fire" data-unicode-name="1F525"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-270A" title="fist" data-aliases="" data-emoji="fist" data-unicode-name="270A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F633" title="flushed" data-aliases="" data-emoji="flushed" data-unicode-name="1F633"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F463" title="footprints" data-aliases="" data-emoji="footprints" data-unicode-name="1F463"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F626" title="frowning" data-aliases=":anguished:" data-emoji="frowning" data-unicode-name="1F626"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F48E" title="gem" data-aliases="" data-emoji="gem" data-unicode-name="1F48E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F467" title="girl" data-aliases="" data-emoji="girl" data-unicode-name="1F467"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F49A" title="green_heart" data-aliases="" data-emoji="green_heart" data-unicode-name="1F49A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F62C" title="grimacing" data-aliases="" data-emoji="grimacing" data-unicode-name="1F62C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F601" title="grin" data-aliases="" data-emoji="grin" data-unicode-name="1F601"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F600" title="grinning" data-aliases="" data-emoji="grinning" data-unicode-name="1F600"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F482" title="guardsman" data-aliases="" data-emoji="guardsman" data-unicode-name="1F482"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F487" title="haircut" data-aliases="" data-emoji="haircut" data-unicode-name="1F487"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F45C" title="handbag" data-aliases="" data-emoji="handbag" data-unicode-name="1F45C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F649" title="hear_no_evil" data-aliases="" data-emoji="hear_no_evil" data-unicode-name="1F649"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-2764" title="heart" data-aliases="" data-emoji="heart" data-unicode-name="2764"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F60D" title="heart_eyes" data-aliases="" data-emoji="heart_eyes" data-unicode-name="1F60D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F63B" title="heart_eyes_cat" data-aliases="" data-emoji="heart_eyes_cat" data-unicode-name="1F63B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F493" title="heartbeat" data-aliases="" data-emoji="heartbeat" data-unicode-name="1F493"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F497" title="heartpulse" data-aliases="" data-emoji="heartpulse" data-unicode-name="1F497"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F460" title="high_heel" data-aliases="" data-emoji="high_heel" data-unicode-name="1F460"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F62F" title="hushed" data-aliases="" data-emoji="hushed" data-unicode-name="1F62F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F47F" title="imp" data-aliases="" data-emoji="imp" data-unicode-name="1F47F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F481" title="information_desk_person" data-aliases="" data-emoji="information_desk_person" data-unicode-name="1F481"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F607" title="innocent" data-aliases="" data-emoji="innocent" data-unicode-name="1F607"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F47A" title="japanese_goblin" data-aliases="" data-emoji="japanese_goblin" data-unicode-name="1F47A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F479" title="japanese_ogre" data-aliases="" data-emoji="japanese_ogre" data-unicode-name="1F479"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F456" title="jeans" data-aliases="" data-emoji="jeans" data-unicode-name="1F456"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F602" title="joy" data-aliases="" data-emoji="joy" data-unicode-name="1F602"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F639" title="joy_cat" data-aliases="" data-emoji="joy_cat" data-unicode-name="1F639"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F458" title="kimono" data-aliases="" data-emoji="kimono" data-unicode-name="1F458"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F48B" title="kiss" data-aliases="" data-emoji="kiss" data-unicode-name="1F48B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F617" title="kissing" data-aliases="" data-emoji="kissing" data-unicode-name="1F617"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F63D" title="kissing_cat" data-aliases="" data-emoji="kissing_cat" data-unicode-name="1F63D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F61A" title="kissing_closed_eyes" data-aliases="" data-emoji="kissing_closed_eyes" data-unicode-name="1F61A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F618" title="kissing_heart" data-aliases="" data-emoji="kissing_heart" data-unicode-name="1F618"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F619" title="kissing_smiling_eyes" data-aliases="" data-emoji="kissing_smiling_eyes" data-unicode-name="1F619"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F606" title="laughing" data-aliases=":satisfied:" data-emoji="laughing" data-unicode-name="1F606"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F444" title="lips" data-aliases="" data-emoji="lips" data-unicode-name="1F444"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F484" title="lipstick" data-aliases="" data-emoji="lipstick" data-unicode-name="1F484"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F48C" title="love_letter" data-aliases="" data-emoji="love_letter" data-unicode-name="1F48C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F468" title="man" data-aliases="" data-emoji="man" data-unicode-name="1F468"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <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> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F473" title="man_with_turban" data-aliases="" data-emoji="man_with_turban" data-unicode-name="1F473"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F45E" title="mans_shoe" data-aliases="" data-emoji="mans_shoe" data-unicode-name="1F45E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F637" title="mask" data-aliases="" data-emoji="mask" data-unicode-name="1F637"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F486" title="massage" data-aliases="" data-emoji="massage" data-unicode-name="1F486"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4AA" title="muscle" data-aliases="" data-emoji="muscle" data-unicode-name="1F4AA"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F485" title="nail_care" data-aliases="" data-emoji="nail_care" data-unicode-name="1F485"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F454" title="necktie" data-aliases="" data-emoji="necktie" data-unicode-name="1F454"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F610" title="neutral_face" data-aliases="" data-emoji="neutral_face" data-unicode-name="1F610"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F645" title="no_good" data-aliases="" data-emoji="no_good" data-unicode-name="1F645"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F636" title="no_mouth" data-aliases="" data-emoji="no_mouth" data-unicode-name="1F636"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F443" title="nose" data-aliases="" data-emoji="nose" data-unicode-name="1F443"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F44C" title="ok_hand" data-aliases="" data-emoji="ok_hand" data-unicode-name="1F44C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F646" title="ok_woman" data-aliases="" data-emoji="ok_woman" data-unicode-name="1F646"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F474" title="older_man" data-aliases="" data-emoji="older_man" data-unicode-name="1F474"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F475" title="older_woman" data-aliases=":grandma:" data-emoji="older_woman" data-unicode-name="1F475"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F450" title="open_hands" data-aliases="" data-emoji="open_hands" data-unicode-name="1F450"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F62E" title="open_mouth" data-aliases="" data-emoji="open_mouth" data-unicode-name="1F62E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F614" title="pensive" data-aliases="" data-emoji="pensive" data-unicode-name="1F614"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F623" title="persevere" data-aliases="" data-emoji="persevere" data-unicode-name="1F623"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F64D" title="person_frowning" data-aliases="" data-emoji="person_frowning" data-unicode-name="1F64D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <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> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <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> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F447" title="point_down" data-aliases="" data-emoji="point_down" data-unicode-name="1F447"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F448" title="point_left" data-aliases="" data-emoji="point_left" data-unicode-name="1F448"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F449" title="point_right" data-aliases="" data-emoji="point_right" data-unicode-name="1F449"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-261D" title="point_up" data-aliases="" data-emoji="point_up" data-unicode-name="261D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F446" title="point_up_2" data-aliases="" data-emoji="point_up_2" data-unicode-name="1F446"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A9" title="poop" data-aliases=":shit: :hankey: :poo:" data-emoji="poop" data-unicode-name="1F4A9"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F45D" title="pouch" data-aliases="" data-emoji="pouch" data-unicode-name="1F45D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F63E" title="pouting_cat" data-aliases="" data-emoji="pouting_cat" data-unicode-name="1F63E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F64F" title="pray" data-aliases="" data-emoji="pray" data-unicode-name="1F64F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F478" title="princess" data-aliases="" data-emoji="princess" data-unicode-name="1F478"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F44A" title="punch" data-aliases="" data-emoji="punch" data-unicode-name="1F44A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F49C" title="purple_heart" data-aliases="" data-emoji="purple_heart" data-unicode-name="1F49C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F45B" title="purse" data-aliases="" data-emoji="purse" data-unicode-name="1F45B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F621" title="rage" data-aliases="" data-emoji="rage" data-unicode-name="1F621"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-270B" title="raised_hand" data-aliases="" data-emoji="raised_hand" data-unicode-name="270B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F64C" title="raised_hands" data-aliases="" data-emoji="raised_hands" data-unicode-name="1F64C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F64B" title="raising_hand" data-aliases="" data-emoji="raising_hand" data-unicode-name="1F64B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-263A" title="relaxed" data-aliases="" data-emoji="relaxed" data-unicode-name="263A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F60C" title="relieved" data-aliases="" data-emoji="relieved" data-unicode-name="1F60C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F49E" title="revolving_hearts" data-aliases="" data-emoji="revolving_hearts" data-unicode-name="1F49E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F380" title="ribbon" data-aliases="" data-emoji="ribbon" data-unicode-name="1F380"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F48D" title="ring" data-aliases="" data-emoji="ring" data-unicode-name="1F48D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F3C3" title="runner" data-aliases="" data-emoji="runner" data-unicode-name="1F3C3"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <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> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F461" title="sandal" data-aliases="" data-emoji="sandal" data-unicode-name="1F461"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F631" title="scream" data-aliases="" data-emoji="scream" data-unicode-name="1F631"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F640" title="scream_cat" data-aliases="" data-emoji="scream_cat" data-unicode-name="1F640"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F648" title="see_no_evil" data-aliases="" data-emoji="see_no_evil" data-unicode-name="1F648"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F455" title="shirt" data-aliases="" data-emoji="shirt" data-unicode-name="1F455"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F480" title="skull" data-aliases=":skeleton:" data-emoji="skull" data-unicode-name="1F480"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F634" title="sleeping" data-aliases="" data-emoji="sleeping" data-unicode-name="1F634"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F62A" title="sleepy" data-aliases="" data-emoji="sleepy" data-unicode-name="1F62A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F604" title="smile" data-aliases="" data-emoji="smile" data-unicode-name="1F604"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F638" title="smile_cat" data-aliases="" data-emoji="smile_cat" data-unicode-name="1F638"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F603" title="smiley" data-aliases="" data-emoji="smiley" data-unicode-name="1F603"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F63A" title="smiley_cat" data-aliases="" data-emoji="smiley_cat" data-unicode-name="1F63A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F608" title="smiling_imp" data-aliases="" data-emoji="smiling_imp" data-unicode-name="1F608"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F60F" title="smirk" data-aliases="" data-emoji="smirk" data-unicode-name="1F60F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F63C" title="smirk_cat" data-aliases="" data-emoji="smirk_cat" data-unicode-name="1F63C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F62D" title="sob" data-aliases="" data-emoji="sob" data-unicode-name="1F62D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-2728" title="sparkles" data-aliases="" data-emoji="sparkles" data-unicode-name="2728"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F496" title="sparkling_heart" data-aliases="" data-emoji="sparkling_heart" data-unicode-name="1F496"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F64A" title="speak_no_evil" data-aliases="" data-emoji="speak_no_evil" data-unicode-name="1F64A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4AC" title="speech_balloon" data-aliases="" data-emoji="speech_balloon" data-unicode-name="1F4AC"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F31F" title="star2" data-aliases="" data-emoji="star2" data-unicode-name="1F31F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F61B" title="stuck_out_tongue" data-aliases="" data-emoji="stuck_out_tongue" data-unicode-name="1F61B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <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> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <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> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F60E" title="sunglasses" data-aliases="" data-emoji="sunglasses" data-unicode-name="1F60E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F613" title="sweat" data-aliases="" data-emoji="sweat" data-unicode-name="1F613"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A6" title="sweat_drops" data-aliases="" data-emoji="sweat_drops" data-unicode-name="1F4A6"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F605" title="sweat_smile" data-aliases="" data-emoji="sweat_smile" data-unicode-name="1F605"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4AD" title="thought_balloon" data-aliases="" data-emoji="thought_balloon" data-unicode-name="1F4AD"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F44E" title="thumbsdown" data-aliases=":-1:" data-emoji="thumbsdown" data-unicode-name="1F44E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F44D" title="thumbsup" data-aliases=":+1:" data-emoji="thumbsup" data-unicode-name="1F44D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F62B" title="tired_face" data-aliases="" data-emoji="tired_face" data-unicode-name="1F62B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F445" title="tongue" data-aliases="" data-emoji="tongue" data-unicode-name="1F445"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F3A9" title="tophat" data-aliases="" data-emoji="tophat" data-unicode-name="1F3A9"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F624" title="triumph" data-aliases="" data-emoji="triumph" data-unicode-name="1F624"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F495" title="two_hearts" data-aliases="" data-emoji="two_hearts" data-unicode-name="1F495"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <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> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <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> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F612" title="unamused" data-aliases="" data-emoji="unamused" data-unicode-name="1F612"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-270C" title="v" data-aliases="" data-emoji="v" data-unicode-name="270C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F6B6" title="walking" data-aliases="" data-emoji="walking" data-unicode-name="1F6B6"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F44B" title="wave" data-aliases="" data-emoji="wave" data-unicode-name="1F44B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F629" title="weary" data-aliases="" data-emoji="weary" data-unicode-name="1F629"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F609" title="wink" data-aliases="" data-emoji="wink" data-unicode-name="1F609"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F469" title="woman" data-aliases="" data-emoji="woman" data-unicode-name="1F469"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F45A" title="womans_clothes" data-aliases="" data-emoji="womans_clothes" data-unicode-name="1F45A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F452" title="womans_hat" data-aliases="" data-emoji="womans_hat" data-unicode-name="1F452"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F61F" title="worried" data-aliases="" data-emoji="worried" data-unicode-name="1F61F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F49B" title="yellow_heart" data-aliases="" data-emoji="yellow_heart" data-unicode-name="1F49B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F60B" title="yum" data-aliases="" data-emoji="yum" data-unicode-name="1F60B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A4" title="zzz" data-aliases="" data-emoji="zzz" data-unicode-name="1F4A4"></div> - </button> - </li> - </ul> - </div> - </div> -""" diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js new file mode 100644 index 00000000000..99e3f7247bd --- /dev/null +++ b/spec/javascripts/fixtures/emoji_menu.js @@ -0,0 +1,4 @@ +(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); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js new file mode 100644 index 00000000000..dc6231ebb38 --- /dev/null +++ b/spec/javascripts/issue_spec.js @@ -0,0 +1,121 @@ + +/*= require lib/utils/text_utility */ + + +/*= require issue */ + +(function() { + describe('Issue', function() { + return describe('task lists', function() { + fixture.preload('issues_show.html'); + beforeEach(function() { + fixture.load('issues_show.html'); + return this.issue = new Issue(); + }); + it('modifies the Markdown field', function() { + spyOn(jQuery, 'ajax').and.stub(); + $('input[type=checkbox]').attr('checked', true).trigger('change'); + return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); + }); + 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'); + return expect(req.data.issue.description).not.toBe(null); + }); + return $('.js-task-list-field').trigger('tasklist:changed'); + }); + }); + }); + + describe('reopen/close issue', function() { + fixture.preload('issues_show.html'); + beforeEach(function() { + fixture.load('issues_show.html'); + return this.issue = new Issue(); + }); + it('closes an issue', function() { + var $btnClose, $btnReopen; + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PUT'); + expect(req.url).toBe('http://gitlab.com/issues/6/close'); + return req.success({ + id: 34 + }); + }); + $btnClose = $('a.btn-close'); + $btnReopen = $('a.btn-reopen'); + expect($btnReopen).toBeHidden(); + expect($btnClose.text()).toBe('Close'); + expect(typeof $btnClose.prop('disabled')).toBe('undefined'); + $btnClose.trigger('click'); + expect($btnReopen).toBeVisible(); + expect($btnClose).toBeHidden(); + expect($('div.status-box-closed')).toBeVisible(); + return expect($('div.status-box-open')).toBeHidden(); + }); + it('fails to close an issue with success:false', function() { + var $btnClose, $btnReopen; + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PUT'); + expect(req.url).toBe('http://goesnowhere.nothing/whereami'); + return req.success({ + saved: false + }); + }); + $btnClose = $('a.btn-close'); + $btnReopen = $('a.btn-reopen'); + $btnClose.attr('href', 'http://goesnowhere.nothing/whereami'); + expect($btnReopen).toBeHidden(); + expect($btnClose.text()).toBe('Close'); + expect(typeof $btnClose.prop('disabled')).toBe('undefined'); + $btnClose.trigger('click'); + expect($btnReopen).toBeHidden(); + expect($btnClose).toBeVisible(); + expect($('div.status-box-closed')).toBeHidden(); + expect($('div.status-box-open')).toBeVisible(); + expect($('div.flash-alert')).toBeVisible(); + return expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.'); + }); + it('fails to closes an issue with HTTP error', function() { + var $btnClose, $btnReopen; + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PUT'); + expect(req.url).toBe('http://goesnowhere.nothing/whereami'); + return req.error(); + }); + $btnClose = $('a.btn-close'); + $btnReopen = $('a.btn-reopen'); + $btnClose.attr('href', 'http://goesnowhere.nothing/whereami'); + expect($btnReopen).toBeHidden(); + expect($btnClose.text()).toBe('Close'); + expect(typeof $btnClose.prop('disabled')).toBe('undefined'); + $btnClose.trigger('click'); + expect($btnReopen).toBeHidden(); + expect($btnClose).toBeVisible(); + expect($('div.status-box-closed')).toBeHidden(); + expect($('div.status-box-open')).toBeVisible(); + expect($('div.flash-alert')).toBeVisible(); + return expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.'); + }); + return it('reopens an issue', function() { + var $btnClose, $btnReopen; + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PUT'); + expect(req.url).toBe('http://gitlab.com/issues/6/reopen'); + return req.success({ + id: 34 + }); + }); + $btnClose = $('a.btn-close'); + $btnReopen = $('a.btn-reopen'); + expect($btnReopen.text()).toBe('Reopen'); + $btnReopen.trigger('click'); + expect($btnReopen).toBeHidden(); + expect($btnClose).toBeVisible(); + expect($('div.status-box-open')).toBeVisible(); + return expect($('div.status-box-closed')).toBeHidden(); + }); + }); + +}).call(this); diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee deleted file mode 100644 index d84d80f266b..00000000000 --- a/spec/javascripts/issue_spec.js.coffee +++ /dev/null @@ -1,109 +0,0 @@ -#= require lib/utils/text_utility -#= require issue - -describe 'Issue', -> - describe 'task lists', -> - fixture.preload('issues_show.html') - - beforeEach -> - fixture.load('issues_show.html') - @issue = new Issue() - - it 'modifies the Markdown field', -> - spyOn(jQuery, 'ajax').and.stub() - $('input[type=checkbox]').attr('checked', true).trigger('change') - expect($('.js-task-list-field').val()).toBe('- [x] Task List Item') - - it 'submits an ajax request on tasklist:changed', -> - spyOn(jQuery, 'ajax').and.callFake (req) -> - expect(req.type).toBe('PATCH') - expect(req.url).toBe('/foo') - expect(req.data.issue.description).not.toBe(null) - - $('.js-task-list-field').trigger('tasklist:changed') -describe 'reopen/close issue', -> - fixture.preload('issues_show.html') - beforeEach -> - fixture.load('issues_show.html') - @issue = new Issue() - it 'closes an issue', -> - spyOn(jQuery, 'ajax').and.callFake (req) -> - expect(req.type).toBe('PUT') - expect(req.url).toBe('http://gitlab.com/issues/6/close') - req.success id: 34 - - $btnClose = $('a.btn-close') - $btnReopen = $('a.btn-reopen') - expect($btnReopen).toBeHidden() - expect($btnClose.text()).toBe('Close') - expect(typeof $btnClose.prop('disabled')).toBe('undefined') - - $btnClose.trigger('click') - - expect($btnReopen).toBeVisible() - expect($btnClose).toBeHidden() - expect($('div.status-box-closed')).toBeVisible() - expect($('div.status-box-open')).toBeHidden() - - it 'fails to close an issue with success:false', -> - - spyOn(jQuery, 'ajax').and.callFake (req) -> - expect(req.type).toBe('PUT') - expect(req.url).toBe('http://goesnowhere.nothing/whereami') - req.success saved: false - - $btnClose = $('a.btn-close') - $btnReopen = $('a.btn-reopen') - $btnClose.attr('href','http://goesnowhere.nothing/whereami') - expect($btnReopen).toBeHidden() - expect($btnClose.text()).toBe('Close') - expect(typeof $btnClose.prop('disabled')).toBe('undefined') - - $btnClose.trigger('click') - - expect($btnReopen).toBeHidden() - expect($btnClose).toBeVisible() - expect($('div.status-box-closed')).toBeHidden() - expect($('div.status-box-open')).toBeVisible() - expect($('div.flash-alert')).toBeVisible() - expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.') - - it 'fails to closes an issue with HTTP error', -> - - spyOn(jQuery, 'ajax').and.callFake (req) -> - expect(req.type).toBe('PUT') - expect(req.url).toBe('http://goesnowhere.nothing/whereami') - req.error() - - $btnClose = $('a.btn-close') - $btnReopen = $('a.btn-reopen') - $btnClose.attr('href','http://goesnowhere.nothing/whereami') - expect($btnReopen).toBeHidden() - expect($btnClose.text()).toBe('Close') - expect(typeof $btnClose.prop('disabled')).toBe('undefined') - - $btnClose.trigger('click') - - expect($btnReopen).toBeHidden() - expect($btnClose).toBeVisible() - expect($('div.status-box-closed')).toBeHidden() - expect($('div.status-box-open')).toBeVisible() - expect($('div.flash-alert')).toBeVisible() - expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.') - - it 'reopens an issue', -> - spyOn(jQuery, 'ajax').and.callFake (req) -> - expect(req.type).toBe('PUT') - expect(req.url).toBe('http://gitlab.com/issues/6/reopen') - req.success id: 34 - - $btnClose = $('a.btn-close') - $btnReopen = $('a.btn-reopen') - expect($btnReopen.text()).toBe('Reopen') - - $btnReopen.trigger('click') - - expect($btnReopen).toBeHidden() - expect($btnClose).toBeVisible() - expect($('div.status-box-open')).toBeVisible() - expect($('div.status-box-closed')).toBeHidden() diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js new file mode 100644 index 00000000000..e2789571607 --- /dev/null +++ b/spec/javascripts/line_highlighter_spec.js @@ -0,0 +1,229 @@ + +/*= require line_highlighter */ + +(function() { + describe('LineHighlighter', function() { + var clickLine; + fixture.preload('line_highlighter.html'); + clickLine = function(number, eventData) { + var e; + if (eventData == null) { + eventData = {}; + } + if ($.isEmptyObject(eventData)) { + return $("#L" + number).mousedown().click(); + } else { + e = $.Event('mousedown', eventData); + return $("#L" + number).trigger(e).click(); + } + }; + beforeEach(function() { + fixture.load('line_highlighter.html'); + this["class"] = new LineHighlighter(); + this.css = this["class"].highlightClass; + return this.spies = { + __setLocationHash__: spyOn(this["class"], '__setLocationHash__').and.callFake(function() {}) + }; + }); + describe('behavior', function() { + it('highlights one line given in the URL hash', function() { + new LineHighlighter('#L13'); + return expect($('#LC13')).toHaveClass(this.css); + }); + it('highlights a range of lines given in the URL hash', function() { + var i, line, results; + new LineHighlighter('#L5-25'); + expect($("." + this.css).length).toBe(21); + results = []; + for (line = i = 5; i <= 25; line = ++i) { + results.push(expect($("#LC" + line)).toHaveClass(this.css)); + } + return results; + }); + it('scrolls to the first highlighted line on initial load', function() { + var spy; + spy = spyOn($, 'scrollTo'); + new LineHighlighter('#L5-25'); + return expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything()); + }); + it('discards click events', function() { + var spy; + spy = spyOnEvent('a[data-line-number]', 'click'); + clickLine(13); + return expect(spy).toHaveBeenPrevented(); + }); + return it('handles garbage input from the hash', function() { + var func; + func = function() { + return new LineHighlighter('#blob-content-holder'); + }; + return expect(func).not.toThrow(); + }); + }); + describe('#clickHandler', function() { + it('discards the mousedown event', function() { + var spy; + spy = spyOnEvent('a[data-line-number]', 'mousedown'); + clickLine(13); + return expect(spy).toHaveBeenPrevented(); + }); + it('handles clicking on a child icon element', function() { + var spy; + spy = spyOn(this["class"], 'setHash').and.callThrough(); + $('#L13 i').mousedown().click(); + expect(spy).toHaveBeenCalledWith(13); + return expect($('#LC13')).toHaveClass(this.css); + }); + describe('without shiftKey', function() { + it('highlights one line when clicked', function() { + clickLine(13); + return expect($('#LC13')).toHaveClass(this.css); + }); + it('unhighlights previously highlighted lines', function() { + clickLine(13); + clickLine(20); + expect($('#LC13')).not.toHaveClass(this.css); + return expect($('#LC20')).toHaveClass(this.css); + }); + return it('sets the hash', function() { + var spy; + spy = spyOn(this["class"], 'setHash').and.callThrough(); + clickLine(13); + return expect(spy).toHaveBeenCalledWith(13); + }); + }); + return describe('with shiftKey', function() { + it('sets the hash', function() { + var spy; + spy = spyOn(this["class"], 'setHash').and.callThrough(); + clickLine(13); + clickLine(20, { + shiftKey: true + }); + expect(spy).toHaveBeenCalledWith(13); + return expect(spy).toHaveBeenCalledWith(13, 20); + }); + describe('without existing highlight', function() { + it('highlights the clicked line', function() { + clickLine(13, { + shiftKey: true + }); + expect($('#LC13')).toHaveClass(this.css); + return expect($("." + this.css).length).toBe(1); + }); + return it('sets the hash', function() { + var spy; + spy = spyOn(this["class"], 'setHash'); + clickLine(13, { + shiftKey: true + }); + return expect(spy).toHaveBeenCalledWith(13); + }); + }); + describe('with existing single-line highlight', function() { + it('uses existing line as last line when target is lesser', function() { + var i, line, results; + clickLine(20); + clickLine(15, { + shiftKey: true + }); + expect($("." + this.css).length).toBe(6); + results = []; + for (line = i = 15; i <= 20; line = ++i) { + results.push(expect($("#LC" + line)).toHaveClass(this.css)); + } + return results; + }); + return it('uses existing line as first line when target is greater', function() { + var i, line, results; + clickLine(5); + clickLine(10, { + shiftKey: true + }); + expect($("." + this.css).length).toBe(6); + results = []; + for (line = i = 5; i <= 10; line = ++i) { + results.push(expect($("#LC" + line)).toHaveClass(this.css)); + } + return results; + }); + }); + return describe('with existing multi-line highlight', function() { + beforeEach(function() { + clickLine(10, { + shiftKey: true + }); + return clickLine(13, { + shiftKey: true + }); + }); + it('uses target as first line when it is less than existing first line', function() { + var i, line, results; + clickLine(5, { + shiftKey: true + }); + expect($("." + this.css).length).toBe(6); + results = []; + for (line = i = 5; i <= 10; line = ++i) { + results.push(expect($("#LC" + line)).toHaveClass(this.css)); + } + return results; + }); + return it('uses target as last line when it is greater than existing first line', function() { + var i, line, results; + clickLine(15, { + shiftKey: true + }); + expect($("." + this.css).length).toBe(6); + results = []; + for (line = i = 10; i <= 15; line = ++i) { + results.push(expect($("#LC" + line)).toHaveClass(this.css)); + } + return results; + }); + }); + }); + }); + describe('#hashToRange', function() { + beforeEach(function() { + return this.subject = this["class"].hashToRange; + }); + it('extracts a single line number from the hash', function() { + return expect(this.subject('#L5')).toEqual([5, null]); + }); + it('extracts a range of line numbers from the hash', function() { + return expect(this.subject('#L5-15')).toEqual([5, 15]); + }); + return it('returns [null, null] when the hash is not a line number', function() { + return expect(this.subject('#foo')).toEqual([null, null]); + }); + }); + describe('#highlightLine', function() { + beforeEach(function() { + return this.subject = this["class"].highlightLine; + }); + it('highlights the specified line', function() { + this.subject(13); + return expect($('#LC13')).toHaveClass(this.css); + }); + return it('accepts a String-based number', function() { + this.subject('13'); + return expect($('#LC13')).toHaveClass(this.css); + }); + }); + return describe('#setHash', function() { + beforeEach(function() { + return this.subject = this["class"].setHash; + }); + it('sets the location hash for a single line', function() { + this.subject(5); + return expect(this.spies.__setLocationHash__).toHaveBeenCalledWith('#L5'); + }); + return it('sets the location hash for a range', function() { + this.subject(5, 15); + return expect(this.spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15'); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/line_highlighter_spec.js.coffee b/spec/javascripts/line_highlighter_spec.js.coffee deleted file mode 100644 index a073f21e7bc..00000000000 --- a/spec/javascripts/line_highlighter_spec.js.coffee +++ /dev/null @@ -1,158 +0,0 @@ -#= require line_highlighter - -describe 'LineHighlighter', -> - fixture.preload('line_highlighter.html') - - clickLine = (number, eventData = {}) -> - if $.isEmptyObject(eventData) - $("#L#{number}").mousedown().click() - else - e = $.Event 'mousedown', eventData - $("#L#{number}").trigger(e).click() - - beforeEach -> - fixture.load('line_highlighter.html') - @class = new LineHighlighter() - @css = @class.highlightClass - @spies = { - __setLocationHash__: spyOn(@class, '__setLocationHash__').and.callFake -> - } - - describe 'behavior', -> - it 'highlights one line given in the URL hash', -> - new LineHighlighter('#L13') - expect($('#LC13')).toHaveClass(@css) - - it 'highlights a range of lines given in the URL hash', -> - new LineHighlighter('#L5-25') - expect($(".#{@css}").length).toBe(21) - expect($("#LC#{line}")).toHaveClass(@css) for line in [5..25] - - it 'scrolls to the first highlighted line on initial load', -> - spy = spyOn($, 'scrollTo') - new LineHighlighter('#L5-25') - expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything()) - - it 'discards click events', -> - spy = spyOnEvent('a[data-line-number]', 'click') - clickLine(13) - expect(spy).toHaveBeenPrevented() - - it 'handles garbage input from the hash', -> - func = -> new LineHighlighter('#blob-content-holder') - expect(func).not.toThrow() - - describe '#clickHandler', -> - it 'discards the mousedown event', -> - spy = spyOnEvent('a[data-line-number]', 'mousedown') - clickLine(13) - expect(spy).toHaveBeenPrevented() - - it 'handles clicking on a child icon element', -> - spy = spyOn(@class, 'setHash').and.callThrough() - - $('#L13 i').mousedown().click() - - expect(spy).toHaveBeenCalledWith(13) - expect($('#LC13')).toHaveClass(@css) - - describe 'without shiftKey', -> - it 'highlights one line when clicked', -> - clickLine(13) - expect($('#LC13')).toHaveClass(@css) - - it 'unhighlights previously highlighted lines', -> - clickLine(13) - clickLine(20) - - expect($('#LC13')).not.toHaveClass(@css) - expect($('#LC20')).toHaveClass(@css) - - it 'sets the hash', -> - spy = spyOn(@class, 'setHash').and.callThrough() - clickLine(13) - expect(spy).toHaveBeenCalledWith(13) - - describe 'with shiftKey', -> - it 'sets the hash', -> - spy = spyOn(@class, 'setHash').and.callThrough() - clickLine(13) - clickLine(20, shiftKey: true) - expect(spy).toHaveBeenCalledWith(13) - expect(spy).toHaveBeenCalledWith(13, 20) - - describe 'without existing highlight', -> - it 'highlights the clicked line', -> - clickLine(13, shiftKey: true) - expect($('#LC13')).toHaveClass(@css) - expect($(".#{@css}").length).toBe(1) - - it 'sets the hash', -> - spy = spyOn(@class, 'setHash') - clickLine(13, shiftKey: true) - expect(spy).toHaveBeenCalledWith(13) - - describe 'with existing single-line highlight', -> - it 'uses existing line as last line when target is lesser', -> - clickLine(20) - clickLine(15, shiftKey: true) - expect($(".#{@css}").length).toBe(6) - expect($("#LC#{line}")).toHaveClass(@css) for line in [15..20] - - it 'uses existing line as first line when target is greater', -> - clickLine(5) - clickLine(10, shiftKey: true) - expect($(".#{@css}").length).toBe(6) - expect($("#LC#{line}")).toHaveClass(@css) for line in [5..10] - - describe 'with existing multi-line highlight', -> - beforeEach -> - clickLine(10, shiftKey: true) - clickLine(13, shiftKey: true) - - it 'uses target as first line when it is less than existing first line', -> - clickLine(5, shiftKey: true) - expect($(".#{@css}").length).toBe(6) - expect($("#LC#{line}")).toHaveClass(@css) for line in [5..10] - - it 'uses target as last line when it is greater than existing first line', -> - clickLine(15, shiftKey: true) - expect($(".#{@css}").length).toBe(6) - expect($("#LC#{line}")).toHaveClass(@css) for line in [10..15] - - describe '#hashToRange', -> - beforeEach -> - @subject = @class.hashToRange - - it 'extracts a single line number from the hash', -> - expect(@subject('#L5')).toEqual([5, null]) - - it 'extracts a range of line numbers from the hash', -> - expect(@subject('#L5-15')).toEqual([5, 15]) - - it 'returns [null, null] when the hash is not a line number', -> - expect(@subject('#foo')).toEqual([null, null]) - - describe '#highlightLine', -> - beforeEach -> - @subject = @class.highlightLine - - it 'highlights the specified line', -> - @subject(13) - expect($('#LC13')).toHaveClass(@css) - - it 'accepts a String-based number', -> - @subject('13') - expect($('#LC13')).toHaveClass(@css) - - describe '#setHash', -> - beforeEach -> - @subject = @class.setHash - - it 'sets the location hash for a single line', -> - @subject(5) - expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5') - - it 'sets the location hash for a range', -> - @subject(5, 15) - expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15') diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js new file mode 100644 index 00000000000..61830d267a9 --- /dev/null +++ b/spec/javascripts/merge_request_spec.js @@ -0,0 +1,28 @@ + +/*= require merge_request */ + +(function() { + describe('MergeRequest', function() { + return describe('task lists', function() { + fixture.preload('merge_requests_show.html'); + beforeEach(function() { + fixture.load('merge_requests_show.html'); + return this.merge = new MergeRequest(); + }); + it('modifies the Markdown field', function() { + spyOn(jQuery, 'ajax').and.stub(); + $('input[type=checkbox]').attr('checked', true).trigger('change'); + return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); + }); + 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'); + return expect(req.data.merge_request.description).not.toBe(null); + }); + return $('.js-task-list-field').trigger('tasklist:changed'); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/merge_request_spec.js.coffee b/spec/javascripts/merge_request_spec.js.coffee deleted file mode 100644 index 3cb67d51c85..00000000000 --- a/spec/javascripts/merge_request_spec.js.coffee +++ /dev/null @@ -1,23 +0,0 @@ -#= require merge_request - -describe 'MergeRequest', -> - describe 'task lists', -> - fixture.preload('merge_requests_show.html') - - beforeEach -> - fixture.load('merge_requests_show.html') - @merge = new MergeRequest() - - it 'modifies the Markdown field', -> - spyOn(jQuery, 'ajax').and.stub() - - $('input[type=checkbox]').attr('checked', true).trigger('change') - expect($('.js-task-list-field').val()).toBe('- [x] Task List Item') - - it 'submits an ajax request on tasklist:changed', -> - spyOn(jQuery, 'ajax').and.callFake (req) -> - expect(req.type).toBe('PATCH') - expect(req.url).toBe('/foo') - expect(req.data.merge_request.description).not.toBe(null) - - $('.js-task-list-field').trigger('tasklist:changed') diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js new file mode 100644 index 00000000000..395032a7416 --- /dev/null +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -0,0 +1,106 @@ + +/*= require merge_request_tabs */ + +(function() { + describe('MergeRequestTabs', function() { + var stubLocation; + stubLocation = function(stubs) { + var defaults; + defaults = { + pathname: '', + search: '', + hash: '' + }; + return $.extend(defaults, stubs); + }; + fixture.preload('merge_request_tabs.html'); + beforeEach(function() { + this["class"] = new MergeRequestTabs(); + return this.spies = { + ajax: spyOn($, 'ajax').and.callFake(function() {}), + history: spyOn(history, 'replaceState').and.callFake(function() {}) + }; + }); + describe('#activateTab', function() { + beforeEach(function() { + fixture.load('merge_request_tabs.html'); + return this.subject = this["class"].activateTab; + }); + it('shows the first tab when action is show', function() { + this.subject('show'); + return expect($('#notes')).toHaveClass('active'); + }); + it('shows the notes tab when action is notes', function() { + this.subject('notes'); + return expect($('#notes')).toHaveClass('active'); + }); + it('shows the commits tab when action is commits', function() { + this.subject('commits'); + return expect($('#commits')).toHaveClass('active'); + }); + return it('shows the diffs tab when action is diffs', function() { + this.subject('diffs'); + return expect($('#diffs')).toHaveClass('active'); + }); + }); + return describe('#setCurrentAction', function() { + beforeEach(function() { + return this.subject = this["class"].setCurrentAction; + }); + it('changes from commits', function() { + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1/commits' + }); + expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); + return expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); + }); + it('changes from diffs', function() { + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1/diffs' + }); + expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); + return expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + }); + it('changes from diffs.html', function() { + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1/diffs.html' + }); + expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); + return expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + }); + it('changes from notes', function() { + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1' + }); + expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); + return expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + }); + it('includes search parameters and hash string', function() { + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1/diffs', + search: '?view=parallel', + hash: '#L15-35' + }); + return expect(this.subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35'); + }); + it('replaces the current history state', function() { + var new_state; + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1' + }); + new_state = this.subject('commits'); + return expect(this.spies.history).toHaveBeenCalledWith({ + turbolinks: true, + url: new_state + }, document.title, new_state); + }); + return it('treats "show" like "notes"', function() { + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1/commits' + }); + return expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/merge_request_tabs_spec.js.coffee b/spec/javascripts/merge_request_tabs_spec.js.coffee deleted file mode 100644 index a0cfba455ea..00000000000 --- a/spec/javascripts/merge_request_tabs_spec.js.coffee +++ /dev/null @@ -1,88 +0,0 @@ -#= require merge_request_tabs - -describe 'MergeRequestTabs', -> - stubLocation = (stubs) -> - defaults = {pathname: '', search: '', hash: ''} - $.extend(defaults, stubs) - - fixture.preload('merge_request_tabs.html') - - beforeEach -> - @class = new MergeRequestTabs() - @spies = { - ajax: spyOn($, 'ajax').and.callFake -> - history: spyOn(history, 'replaceState').and.callFake -> - } - - describe '#activateTab', -> - beforeEach -> - fixture.load('merge_request_tabs.html') - @subject = @class.activateTab - - it 'shows the first tab when action is show', -> - @subject('show') - expect($('#notes')).toHaveClass('active') - - it 'shows the notes tab when action is notes', -> - @subject('notes') - expect($('#notes')).toHaveClass('active') - - it 'shows the commits tab when action is commits', -> - @subject('commits') - expect($('#commits')).toHaveClass('active') - - it 'shows the diffs tab when action is diffs', -> - @subject('diffs') - expect($('#diffs')).toHaveClass('active') - - describe '#setCurrentAction', -> - beforeEach -> - @subject = @class.setCurrentAction - - it 'changes from commits', -> - @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/commits') - - expect(@subject('notes')).toBe('/foo/bar/merge_requests/1') - expect(@subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs') - - it 'changes from diffs', -> - @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/diffs') - - expect(@subject('notes')).toBe('/foo/bar/merge_requests/1') - expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits') - - it 'changes from diffs.html', -> - @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/diffs.html') - - expect(@subject('notes')).toBe('/foo/bar/merge_requests/1') - expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits') - - it 'changes from notes', -> - @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1') - - expect(@subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs') - expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits') - - it 'includes search parameters and hash string', -> - @class._location = stubLocation({ - pathname: '/foo/bar/merge_requests/1/diffs' - search: '?view=parallel' - hash: '#L15-35' - }) - - expect(@subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35') - - it 'replaces the current history state', -> - @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1') - new_state = @subject('commits') - - expect(@spies.history).toHaveBeenCalledWith( - {turbolinks: true, url: new_state}, - document.title, - new_state - ) - - it 'treats "show" like "notes"', -> - @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/commits') - - expect(@subject('show')).toBe('/foo/bar/merge_requests/1') diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js new file mode 100644 index 00000000000..17b32914ec3 --- /dev/null +++ b/spec/javascripts/merge_request_widget_spec.js @@ -0,0 +1,74 @@ + +/*= require merge_request_widget */ + +(function() { + describe('MergeRequestWidget', function() { + beforeEach(function() { + window.notifyPermissions = function() {}; + window.notify = function() {}; + this.opts = { + ci_status_url: "http://sampledomain.local/ci/getstatus", + ci_status: "", + ci_message: { + normal: "Build {{status}} for \"{{title}}\"", + preparing: "{{status}} build for \"{{title}}\"" + }, + ci_title: { + preparing: "{{status}} build", + normal: "Build {{status}}" + }, + gitlab_icon: "gitlab_logo.png", + builds_path: "http://sampledomain.local/sampleBuildsPath" + }; + this["class"] = new MergeRequestWidget(this.opts); + return this.ciStatusData = { + "title": "Sample MR title", + "sha": "12a34bc5", + "status": "success", + "coverage": 98 + }; + }); + return describe('getCIStatus', function() { + beforeEach(function() { + return spyOn(jQuery, 'getJSON').and.callFake((function(_this) { + return function(req, cb) { + return cb(_this.ciStatusData); + }; + })(this)); + }); + it('should call showCIStatus even if a notification should not be displayed', function() { + var spy; + spy = spyOn(this["class"], 'showCIStatus').and.stub(); + this["class"].getCIStatus(false); + return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status); + }); + it('should call showCIStatus when a notification should be displayed', function() { + var spy; + spy = spyOn(this["class"], 'showCIStatus').and.stub(); + this["class"].getCIStatus(true); + return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status); + }); + it('should call showCICoverage when the coverage rate is set', function() { + var spy; + spy = spyOn(this["class"], 'showCICoverage').and.stub(); + this["class"].getCIStatus(false); + return expect(spy).toHaveBeenCalledWith(this.ciStatusData.coverage); + }); + it('should not call showCICoverage when the coverage rate is not set', function() { + var spy; + this.ciStatusData.coverage = null; + spy = spyOn(this["class"], 'showCICoverage').and.stub(); + this["class"].getCIStatus(false); + return expect(spy).not.toHaveBeenCalled(); + }); + return it('should not display a notification on the first check after the widget has been created', function() { + var spy; + spy = spyOn(window, 'notify'); + this["class"] = new MergeRequestWidget(this.opts); + this["class"].getCIStatus(true); + return expect(spy).not.toHaveBeenCalled(); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/merge_request_widget_spec.js.coffee b/spec/javascripts/merge_request_widget_spec.js.coffee deleted file mode 100644 index 92b7eeb1116..00000000000 --- a/spec/javascripts/merge_request_widget_spec.js.coffee +++ /dev/null @@ -1,55 +0,0 @@ -#= require merge_request_widget - -describe 'MergeRequestWidget', -> - - beforeEach -> - window.notifyPermissions = () -> - window.notify = () -> - @opts = { - ci_status_url:"http://sampledomain.local/ci/getstatus", - ci_status:"", - ci_message: { - normal: "Build {{status}} for \"{{title}}\"", - preparing: "{{status}} build for \"{{title}}\"" - }, - ci_title: { - preparing: "{{status}} build", - normal: "Build {{status}}" - }, - gitlab_icon:"gitlab_logo.png", - builds_path:"http://sampledomain.local/sampleBuildsPath" - } - @class = new MergeRequestWidget(@opts) - @ciStatusData = {"title":"Sample MR title","sha":"12a34bc5","status":"success","coverage":98} - - describe 'getCIStatus', -> - beforeEach -> - spyOn(jQuery, 'getJSON').and.callFake (req, cb) => - cb(@ciStatusData) - - it 'should call showCIStatus even if a notification should not be displayed', -> - spy = spyOn(@class, 'showCIStatus').and.stub() - @class.getCIStatus(false) - expect(spy).toHaveBeenCalledWith(@ciStatusData.status) - - it 'should call showCIStatus when a notification should be displayed', -> - spy = spyOn(@class, 'showCIStatus').and.stub() - @class.getCIStatus(true) - expect(spy).toHaveBeenCalledWith(@ciStatusData.status) - - it 'should call showCICoverage when the coverage rate is set', -> - spy = spyOn(@class, 'showCICoverage').and.stub() - @class.getCIStatus(false) - expect(spy).toHaveBeenCalledWith(@ciStatusData.coverage) - - it 'should not call showCICoverage when the coverage rate is not set', -> - @ciStatusData.coverage = null - spy = spyOn(@class, 'showCICoverage').and.stub() - @class.getCIStatus(false) - expect(spy).not.toHaveBeenCalled() - - it 'should not display a notification on the first check after the widget has been created', -> - spy = spyOn(window, 'notify') - @class = new MergeRequestWidget(@opts) - @class.getCIStatus(true) - expect(spy).not.toHaveBeenCalled() diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js new file mode 100644 index 00000000000..25d3f5b6c04 --- /dev/null +++ b/spec/javascripts/new_branch_spec.js @@ -0,0 +1,170 @@ + +/*= require jquery-ui/autocomplete */ + + +/*= require new_branch_form */ + +(function() { + describe('Branch', function() { + return describe('create a new branch', function() { + var expectToHaveError, fillNameWith; + fixture.preload('new_branch.html'); + fillNameWith = function(value) { + return $('.js-branch-name').val(value).trigger('blur'); + }; + expectToHaveError = function(error) { + return expect($('.js-branch-name-error span').text()).toEqual(error); + }; + beforeEach(function() { + fixture.load('new_branch.html'); + $('form').on('submit', function(e) { + return e.preventDefault(); + }); + return this.form = new NewBranchForm($('.js-create-branch-form'), []); + }); + it("can't start with a dot", function() { + fillNameWith('.foo'); + return expectToHaveError("can't start with '.'"); + }); + it("can't start with a slash", function() { + fillNameWith('/foo'); + return expectToHaveError("can't start with '/'"); + }); + it("can't have two consecutive dots", function() { + fillNameWith('foo..bar'); + return expectToHaveError("can't contain '..'"); + }); + it("can't have spaces anywhere", function() { + fillNameWith(' foo'); + expectToHaveError("can't contain spaces"); + fillNameWith('foo bar'); + expectToHaveError("can't contain spaces"); + fillNameWith('foo '); + return expectToHaveError("can't contain spaces"); + }); + it("can't have ~ anywhere", function() { + fillNameWith('~foo'); + expectToHaveError("can't contain '~'"); + fillNameWith('foo~bar'); + expectToHaveError("can't contain '~'"); + fillNameWith('foo~'); + return expectToHaveError("can't contain '~'"); + }); + it("can't have tilde anwhere", function() { + fillNameWith('~foo'); + expectToHaveError("can't contain '~'"); + fillNameWith('foo~bar'); + expectToHaveError("can't contain '~'"); + fillNameWith('foo~'); + return expectToHaveError("can't contain '~'"); + }); + it("can't have caret anywhere", function() { + fillNameWith('^foo'); + expectToHaveError("can't contain '^'"); + fillNameWith('foo^bar'); + expectToHaveError("can't contain '^'"); + fillNameWith('foo^'); + return expectToHaveError("can't contain '^'"); + }); + it("can't have : anywhere", function() { + fillNameWith(':foo'); + expectToHaveError("can't contain ':'"); + fillNameWith('foo:bar'); + expectToHaveError("can't contain ':'"); + fillNameWith(':foo'); + return expectToHaveError("can't contain ':'"); + }); + it("can't have question mark anywhere", function() { + fillNameWith('?foo'); + expectToHaveError("can't contain '?'"); + fillNameWith('foo?bar'); + expectToHaveError("can't contain '?'"); + fillNameWith('foo?'); + return expectToHaveError("can't contain '?'"); + }); + it("can't have asterisk anywhere", function() { + fillNameWith('*foo'); + expectToHaveError("can't contain '*'"); + fillNameWith('foo*bar'); + expectToHaveError("can't contain '*'"); + fillNameWith('foo*'); + return expectToHaveError("can't contain '*'"); + }); + it("can't have open bracket anywhere", function() { + fillNameWith('[foo'); + expectToHaveError("can't contain '['"); + fillNameWith('foo[bar'); + expectToHaveError("can't contain '['"); + fillNameWith('foo['); + return expectToHaveError("can't contain '['"); + }); + it("can't have a backslash anywhere", function() { + fillNameWith('\\foo'); + expectToHaveError("can't contain '\\'"); + fillNameWith('foo\\bar'); + expectToHaveError("can't contain '\\'"); + fillNameWith('foo\\'); + return expectToHaveError("can't contain '\\'"); + }); + it("can't contain a sequence @{ anywhere", function() { + fillNameWith('@{foo'); + expectToHaveError("can't contain '@{'"); + fillNameWith('foo@{bar'); + expectToHaveError("can't contain '@{'"); + fillNameWith('foo@{'); + return expectToHaveError("can't contain '@{'"); + }); + it("can't have consecutive slashes", function() { + fillNameWith('foo//bar'); + return expectToHaveError("can't contain consecutive slashes"); + }); + it("can't end with a slash", function() { + fillNameWith('foo/'); + return expectToHaveError("can't end in '/'"); + }); + it("can't end with a dot", function() { + fillNameWith('foo.'); + return expectToHaveError("can't end in '.'"); + }); + it("can't end with .lock", function() { + fillNameWith('foo.lock'); + return expectToHaveError("can't end in '.lock'"); + }); + it("can't be the single character @", function() { + fillNameWith('@'); + return expectToHaveError("can't be '@'"); + }); + it("concatenates all error messages", function() { + fillNameWith('/foo bar?~.'); + return expectToHaveError("can't start with '/', can't contain spaces, '?', '~', can't end in '.'"); + }); + it("doesn't duplicate error messages", function() { + fillNameWith('?foo?bar?zoo?'); + return expectToHaveError("can't contain '?'"); + }); + it("removes the error message when is a valid name", function() { + fillNameWith('foo?bar'); + expect($('.js-branch-name-error span').length).toEqual(1); + fillNameWith('foobar'); + return expect($('.js-branch-name-error span').length).toEqual(0); + }); + it("can have dashes anywhere", function() { + fillNameWith('-foo-bar-zoo-'); + return expect($('.js-branch-name-error span').length).toEqual(0); + }); + it("can have underscores anywhere", function() { + fillNameWith('_foo_bar_zoo_'); + return expect($('.js-branch-name-error span').length).toEqual(0); + }); + it("can have numbers anywhere", function() { + fillNameWith('1foo2bar3zoo4'); + return expect($('.js-branch-name-error span').length).toEqual(0); + }); + return it("can be only letters", function() { + fillNameWith('foo'); + return expect($('.js-branch-name-error span').length).toEqual(0); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/new_branch_spec.js.coffee b/spec/javascripts/new_branch_spec.js.coffee deleted file mode 100644 index ce773793817..00000000000 --- a/spec/javascripts/new_branch_spec.js.coffee +++ /dev/null @@ -1,160 +0,0 @@ -#= require jquery-ui/autocomplete -#= require new_branch_form - -describe 'Branch', -> - describe 'create a new branch', -> - fixture.preload('new_branch.html') - - fillNameWith = (value) -> - $('.js-branch-name').val(value).trigger('blur') - - expectToHaveError = (error) -> - expect($('.js-branch-name-error span').text()).toEqual(error) - - beforeEach -> - fixture.load('new_branch.html') - $('form').on 'submit', (e) -> e.preventDefault() - - @form = new NewBranchForm($('.js-create-branch-form'), []) - - it "can't start with a dot", -> - fillNameWith '.foo' - expectToHaveError "can't start with '.'" - - it "can't start with a slash", -> - fillNameWith '/foo' - expectToHaveError "can't start with '/'" - - it "can't have two consecutive dots", -> - fillNameWith 'foo..bar' - expectToHaveError "can't contain '..'" - - it "can't have spaces anywhere", -> - fillNameWith ' foo' - expectToHaveError "can't contain spaces" - fillNameWith 'foo bar' - expectToHaveError "can't contain spaces" - fillNameWith 'foo ' - expectToHaveError "can't contain spaces" - - it "can't have ~ anywhere", -> - fillNameWith '~foo' - expectToHaveError "can't contain '~'" - fillNameWith 'foo~bar' - expectToHaveError "can't contain '~'" - fillNameWith 'foo~' - expectToHaveError "can't contain '~'" - - it "can't have tilde anwhere", -> - fillNameWith '~foo' - expectToHaveError "can't contain '~'" - fillNameWith 'foo~bar' - expectToHaveError "can't contain '~'" - fillNameWith 'foo~' - expectToHaveError "can't contain '~'" - - it "can't have caret anywhere", -> - fillNameWith '^foo' - expectToHaveError "can't contain '^'" - fillNameWith 'foo^bar' - expectToHaveError "can't contain '^'" - fillNameWith 'foo^' - expectToHaveError "can't contain '^'" - - it "can't have : anywhere", -> - fillNameWith ':foo' - expectToHaveError "can't contain ':'" - fillNameWith 'foo:bar' - expectToHaveError "can't contain ':'" - fillNameWith ':foo' - expectToHaveError "can't contain ':'" - - it "can't have question mark anywhere", -> - fillNameWith '?foo' - expectToHaveError "can't contain '?'" - fillNameWith 'foo?bar' - expectToHaveError "can't contain '?'" - fillNameWith 'foo?' - expectToHaveError "can't contain '?'" - - it "can't have asterisk anywhere", -> - fillNameWith '*foo' - expectToHaveError "can't contain '*'" - fillNameWith 'foo*bar' - expectToHaveError "can't contain '*'" - fillNameWith 'foo*' - expectToHaveError "can't contain '*'" - - it "can't have open bracket anywhere", -> - fillNameWith '[foo' - expectToHaveError "can't contain '['" - fillNameWith 'foo[bar' - expectToHaveError "can't contain '['" - fillNameWith 'foo[' - expectToHaveError "can't contain '['" - - it "can't have a backslash anywhere", -> - fillNameWith '\\foo' - expectToHaveError "can't contain '\\'" - fillNameWith 'foo\\bar' - expectToHaveError "can't contain '\\'" - fillNameWith 'foo\\' - expectToHaveError "can't contain '\\'" - - it "can't contain a sequence @{ anywhere", -> - fillNameWith '@{foo' - expectToHaveError "can't contain '@{'" - fillNameWith 'foo@{bar' - expectToHaveError "can't contain '@{'" - fillNameWith 'foo@{' - expectToHaveError "can't contain '@{'" - - it "can't have consecutive slashes", -> - fillNameWith 'foo//bar' - expectToHaveError "can't contain consecutive slashes" - - it "can't end with a slash", -> - fillNameWith 'foo/' - expectToHaveError "can't end in '/'" - - it "can't end with a dot", -> - fillNameWith 'foo.' - expectToHaveError "can't end in '.'" - - it "can't end with .lock", -> - fillNameWith 'foo.lock' - expectToHaveError "can't end in '.lock'" - - it "can't be the single character @", -> - fillNameWith '@' - expectToHaveError "can't be '@'" - - it "concatenates all error messages", -> - fillNameWith '/foo bar?~.' - expectToHaveError "can't start with '/', can't contain spaces, '?', '~', can't end in '.'" - - it "doesn't duplicate error messages", -> - fillNameWith '?foo?bar?zoo?' - expectToHaveError "can't contain '?'" - - it "removes the error message when is a valid name", -> - fillNameWith 'foo?bar' - expect($('.js-branch-name-error span').length).toEqual(1) - fillNameWith 'foobar' - expect($('.js-branch-name-error span').length).toEqual(0) - - it "can have dashes anywhere", -> - fillNameWith '-foo-bar-zoo-' - expect($('.js-branch-name-error span').length).toEqual(0) - - it "can have underscores anywhere", -> - fillNameWith '_foo_bar_zoo_' - expect($('.js-branch-name-error span').length).toEqual(0) - - it "can have numbers anywhere", -> - fillNameWith '1foo2bar3zoo4' - expect($('.js-branch-name-error span').length).toEqual(0) - - it "can be only letters", -> - fillNameWith 'foo' - expect($('.js-branch-name-error span').length).toEqual(0) diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js new file mode 100644 index 00000000000..14dc6bfdfde --- /dev/null +++ b/spec/javascripts/notes_spec.js @@ -0,0 +1,41 @@ + +/*= require notes */ + + +/*= require gl_form */ + +(function() { + window.gon || (window.gon = {}); + + window.disableButtonIfEmptyField = function() { + return null; + }; + + describe('Notes', function() { + return describe('task lists', function() { + fixture.preload('issue_note.html'); + beforeEach(function() { + fixture.load('issue_note.html'); + $('form').on('submit', function(e) { + return e.preventDefault(); + }); + return this.notes = new Notes(); + }); + it('modifies the Markdown field', function() { + $('input[type=checkbox]').attr('checked', true).trigger('change'); + return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); + }); + return it('submits the form on tasklist:changed', function() { + var submitted; + submitted = false; + $('form').on('submit', function(e) { + submitted = true; + return e.preventDefault(); + }); + $('.js-task-list-field').trigger('tasklist:changed'); + return expect(submitted).toBe(true); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/notes_spec.js.coffee b/spec/javascripts/notes_spec.js.coffee deleted file mode 100644 index 3a3c8d63e82..00000000000 --- a/spec/javascripts/notes_spec.js.coffee +++ /dev/null @@ -1,26 +0,0 @@ -#= require notes -#= require gl_form - -window.gon or= {} -window.disableButtonIfEmptyField = -> null - -describe 'Notes', -> - describe 'task lists', -> - fixture.preload('issue_note.html') - - beforeEach -> - fixture.load('issue_note.html') - $('form').on 'submit', (e) -> e.preventDefault() - - @notes = new Notes() - - it 'modifies the Markdown field', -> - $('input[type=checkbox]').attr('checked', true).trigger('change') - expect($('.js-task-list-field').val()).toBe('- [x] Task List Item') - - it 'submits the form on tasklist:changed', -> - submitted = false - $('form').on 'submit', (e) -> submitted = true; e.preventDefault() - - $('.js-task-list-field').trigger('tasklist:changed') - expect(submitted).toBe(true) diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js new file mode 100644 index 00000000000..ffe49828492 --- /dev/null +++ b/spec/javascripts/project_title_spec.js @@ -0,0 +1,60 @@ + +/*= require bootstrap */ + + +/*= require select2 */ + + +/*= require lib/utils/type_utility */ + + +/*= require gl_dropdown */ + + +/*= require api */ + + +/*= require project_select */ + + +/*= require project */ + +(function() { + window.gon || (window.gon = {}); + + window.gon.api_version = 'v3'; + + describe('Project Title', function() { + fixture.preload('project_title.html'); + fixture.preload('projects.json'); + beforeEach(function() { + fixture.load('project_title.html'); + return this.project = new Project(); + }); + return describe('project list', function() { + beforeEach((function(_this) { + return function() { + _this.projects_data = fixture.load('projects.json')[0]; + return spyOn(jQuery, 'ajax').and.callFake(function(req) { + var d; + expect(req.url).toBe('/api/v3/projects.json?simple=true'); + d = $.Deferred(); + d.resolve(_this.projects_data); + return d.promise(); + }); + }; + })(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); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee deleted file mode 100644 index 0244119fa0e..00000000000 --- a/spec/javascripts/project_title_spec.js.coffee +++ /dev/null @@ -1,37 +0,0 @@ -#= require bootstrap -#= require select2 -#= require lib/utils/type_utility -#= require gl_dropdown -#= require api -#= require project_select -#= require project - -window.gon or= {} -window.gon.api_version = 'v3' - -describe 'Project Title', -> - fixture.preload('project_title.html') - fixture.preload('projects.json') - - beforeEach -> - fixture.load('project_title.html') - @project = new Project() - - describe 'project list', -> - beforeEach => - @projects_data = fixture.load('projects.json')[0] - - spyOn(jQuery, 'ajax').and.callFake (req) => - expect(req.url).toBe('/api/v3/projects.json?simple=true') - d = $.Deferred() - d.resolve @projects_data - d.promise() - - it 'to show on toggle click', => - $('.js-projects-dropdown-toggle').click() - expect($('.header-content').hasClass('open')).toBe(true) - - it 'hide dropdown', -> - $(".dropdown-menu-close-icon").click() - - expect($('.header-content').hasClass('open')).toBe(false) diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js new file mode 100644 index 00000000000..38b3b2653ec --- /dev/null +++ b/spec/javascripts/right_sidebar_spec.js @@ -0,0 +1,70 @@ + +/*= require right_sidebar */ + + +/*= require jquery */ + + +/*= require jquery.cookie */ + +(function() { + var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState; + + this.sidebar = null; + + $aside = null; + + $toggle = null; + + $icon = null; + + $page = null; + + $labelsIcon = null; + + assertSidebarState = function(state) { + var shouldBeCollapsed, shouldBeExpanded; + shouldBeExpanded = state === 'expanded'; + shouldBeCollapsed = state === 'collapsed'; + expect($aside.hasClass('right-sidebar-expanded')).toBe(shouldBeExpanded); + expect($page.hasClass('right-sidebar-expanded')).toBe(shouldBeExpanded); + expect($icon.hasClass('fa-angle-double-right')).toBe(shouldBeExpanded); + expect($aside.hasClass('right-sidebar-collapsed')).toBe(shouldBeCollapsed); + expect($page.hasClass('right-sidebar-collapsed')).toBe(shouldBeCollapsed); + return expect($icon.hasClass('fa-angle-double-left')).toBe(shouldBeCollapsed); + }; + + describe('RightSidebar', function() { + fixture.preload('right_sidebar.html'); + beforeEach(function() { + fixture.load('right_sidebar.html'); + this.sidebar = new Sidebar; + $aside = $('.right-sidebar'); + $page = $('.page-with-sidebar'); + $icon = $aside.find('i'); + $toggle = $aside.find('.js-sidebar-toggle'); + return $labelsIcon = $aside.find('.sidebar-collapsed-icon'); + }); + it('should expand the sidebar when arrow is clicked', function() { + $toggle.click(); + return assertSidebarState('expanded'); + }); + it('should collapse the sidebar when arrow is clicked', function() { + $toggle.click(); + assertSidebarState('expanded'); + $toggle.click(); + return assertSidebarState('collapsed'); + }); + it('should float over the page and when sidebar icons clicked', function() { + $labelsIcon.click(); + return assertSidebarState('expanded'); + }); + return it('should collapse when the icon arrow clicked while it is floating on page', function() { + $labelsIcon.click(); + assertSidebarState('expanded'); + $toggle.click(); + return assertSidebarState('collapsed'); + }); + }); + +}).call(this); diff --git a/spec/javascripts/right_sidebar_spec.js.coffee b/spec/javascripts/right_sidebar_spec.js.coffee deleted file mode 100644 index 2075cacdb67..00000000000 --- a/spec/javascripts/right_sidebar_spec.js.coffee +++ /dev/null @@ -1,69 +0,0 @@ -#= require right_sidebar -#= require jquery -#= require jquery.cookie - -@sidebar = null -$aside = null -$toggle = null -$icon = null -$page = null -$labelsIcon = null - - -assertSidebarState = (state) -> - - shouldBeExpanded = state is 'expanded' - shouldBeCollapsed = state is 'collapsed' - - expect($aside.hasClass('right-sidebar-expanded')).toBe shouldBeExpanded - expect($page.hasClass('right-sidebar-expanded')).toBe shouldBeExpanded - expect($icon.hasClass('fa-angle-double-right')).toBe shouldBeExpanded - - expect($aside.hasClass('right-sidebar-collapsed')).toBe shouldBeCollapsed - expect($page.hasClass('right-sidebar-collapsed')).toBe shouldBeCollapsed - expect($icon.hasClass('fa-angle-double-left')).toBe shouldBeCollapsed - - -describe 'RightSidebar', -> - - fixture.preload 'right_sidebar.html' - - beforeEach -> - fixture.load 'right_sidebar.html' - - @sidebar = new Sidebar - $aside = $ '.right-sidebar' - $page = $ '.page-with-sidebar' - $icon = $aside.find 'i' - $toggle = $aside.find '.js-sidebar-toggle' - $labelsIcon = $aside.find '.sidebar-collapsed-icon' - - - it 'should expand the sidebar when arrow is clicked', -> - - $toggle.click() - assertSidebarState 'expanded' - - - it 'should collapse the sidebar when arrow is clicked', -> - - $toggle.click() - assertSidebarState 'expanded' - - $toggle.click() - assertSidebarState 'collapsed' - - - it 'should float over the page and when sidebar icons clicked', -> - - $labelsIcon.click() - assertSidebarState 'expanded' - - - it 'should collapse when the icon arrow clicked while it is floating on page', -> - - $labelsIcon.click() - assertSidebarState 'expanded' - - $toggle.click() - assertSidebarState 'collapsed' diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js new file mode 100644 index 00000000000..68d64483d67 --- /dev/null +++ b/spec/javascripts/search_autocomplete_spec.js @@ -0,0 +1,159 @@ + +/*= require gl_dropdown */ + + +/*= require search_autocomplete */ + + +/*= require jquery */ + + +/*= require lib/utils/common_utils */ + + +/*= require lib/utils/type_utility */ + + +/*= require fuzzaldrin-plus */ + +(function() { + var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; + + widget = null; + + userId = 1; + + window.gon || (window.gon = {}); + + window.gon.current_user_id = userId; + + dashboardIssuesPath = '/dashboard/issues'; + + dashboardMRsPath = '/dashboard/merge_requests'; + + projectIssuesPath = '/gitlab-org/gitlab-ce/issues'; + + projectMRsPath = '/gitlab-org/gitlab-ce/merge_requests'; + + groupIssuesPath = '/groups/gitlab-org/issues'; + + groupMRsPath = '/groups/gitlab-org/merge_requests'; + + projectName = 'GitLab Community Edition'; + + groupName = 'Gitlab Org'; + + addBodyAttributes = function(section) { + var $body; + if (section == null) { + section = 'dashboard'; + } + $body = $('body'); + $body.removeAttr('data-page'); + $body.removeAttr('data-project'); + $body.removeAttr('data-group'); + switch (section) { + case 'dashboard': + return $body.data('page', 'root:index'); + case 'group': + $body.data('page', 'groups:show'); + return $body.data('group', 'gitlab-org'); + case 'project': + $body.data('page', 'projects:show'); + return $body.data('project', 'gitlab-ce'); + } + }; + + mockDashboardOptions = function() { + window.gl || (window.gl = {}); + return window.gl.dashboardOptions = { + issuesPath: dashboardIssuesPath, + mrPath: dashboardMRsPath + }; + }; + + mockProjectOptions = function() { + window.gl || (window.gl = {}); + return window.gl.projectOptions = { + 'gitlab-ce': { + issuesPath: projectIssuesPath, + mrPath: projectMRsPath, + projectName: projectName + } + }; + }; + + mockGroupOptions = function() { + window.gl || (window.gl = {}); + return window.gl.groupOptions = { + 'gitlab-org': { + issuesPath: groupIssuesPath, + mrPath: groupMRsPath, + projectName: groupName + } + }; + }; + + assertLinks = function(list, issuesPath, mrsPath) { + var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink; + issuesAssignedToMeLink = issuesPath + "/?assignee_id=" + userId; + issuesIHaveCreatedLink = issuesPath + "/?author_id=" + userId; + mrsAssignedToMeLink = mrsPath + "/?assignee_id=" + userId; + mrsIHaveCreatedLink = mrsPath + "/?author_id=" + userId; + a1 = "a[href='" + issuesAssignedToMeLink + "']"; + a2 = "a[href='" + issuesIHaveCreatedLink + "']"; + a3 = "a[href='" + mrsAssignedToMeLink + "']"; + a4 = "a[href='" + mrsIHaveCreatedLink + "']"; + expect(list.find(a1).length).toBe(1); + expect(list.find(a1).text()).toBe(' Issues assigned to me '); + expect(list.find(a2).length).toBe(1); + expect(list.find(a2).text()).toBe(" Issues I've created "); + expect(list.find(a3).length).toBe(1); + expect(list.find(a3).text()).toBe(' Merge requests assigned to me '); + expect(list.find(a4).length).toBe(1); + return expect(list.find(a4).text()).toBe(" Merge requests I've created "); + }; + + describe('Search autocomplete dropdown', function() { + fixture.preload('search_autocomplete.html'); + beforeEach(function() { + fixture.load('search_autocomplete.html'); + return widget = new SearchAutocomplete; + }); + it('should show Dashboard specific dropdown menu', function() { + var list; + addBodyAttributes(); + mockDashboardOptions(); + widget.searchInput.focus(); + list = widget.wrap.find('.dropdown-menu').find('ul'); + return assertLinks(list, dashboardIssuesPath, dashboardMRsPath); + }); + it('should show Group specific dropdown menu', function() { + var list; + addBodyAttributes('group'); + mockGroupOptions(); + widget.searchInput.focus(); + list = widget.wrap.find('.dropdown-menu').find('ul'); + return assertLinks(list, groupIssuesPath, groupMRsPath); + }); + it('should show Project specific dropdown menu', function() { + var list; + addBodyAttributes('project'); + mockProjectOptions(); + widget.searchInput.focus(); + list = widget.wrap.find('.dropdown-menu').find('ul'); + return assertLinks(list, projectIssuesPath, projectMRsPath); + }); + return it('should not show category related menu if there is text in the input', function() { + var link, list; + addBodyAttributes('project'); + mockProjectOptions(); + widget.searchInput.val('help'); + widget.searchInput.focus(); + list = widget.wrap.find('.dropdown-menu').find('ul'); + link = "a[href='" + projectIssuesPath + "/?assignee_id=" + userId + "']"; + return expect(list.find(link).length).toBe(0); + }); + }); + +}).call(this); diff --git a/spec/javascripts/search_autocomplete_spec.js.coffee b/spec/javascripts/search_autocomplete_spec.js.coffee deleted file mode 100644 index 1c1faca3333..00000000000 --- a/spec/javascripts/search_autocomplete_spec.js.coffee +++ /dev/null @@ -1,149 +0,0 @@ -#= require gl_dropdown -#= require search_autocomplete -#= require jquery -#= require lib/utils/common_utils -#= require lib/utils/type_utility -#= require fuzzaldrin-plus - - -widget = null -userId = 1 -window.gon or= {} -window.gon.current_user_id = userId - -dashboardIssuesPath = '/dashboard/issues' -dashboardMRsPath = '/dashboard/merge_requests' -projectIssuesPath = '/gitlab-org/gitlab-ce/issues' -projectMRsPath = '/gitlab-org/gitlab-ce/merge_requests' -groupIssuesPath = '/groups/gitlab-org/issues' -groupMRsPath = '/groups/gitlab-org/merge_requests' -projectName = 'GitLab Community Edition' -groupName = 'Gitlab Org' - - -# Add required attributes to body before starting the test. -# section would be dashboard|group|project -addBodyAttributes = (section = 'dashboard') -> - - $body = $ 'body' - - $body.removeAttr 'data-page' - $body.removeAttr 'data-project' - $body.removeAttr 'data-group' - - switch section - when 'dashboard' - $body.data 'page', 'root:index' - when 'group' - $body.data 'page', 'groups:show' - $body.data 'group', 'gitlab-org' - when 'project' - $body.data 'page', 'projects:show' - $body.data 'project', 'gitlab-ce' - - -# Mock `gl` object in window for dashboard specific page. App code will need it. -mockDashboardOptions = -> - - window.gl or= {} - window.gl.dashboardOptions = - issuesPath: dashboardIssuesPath - mrPath : dashboardMRsPath - - -# Mock `gl` object in window for project specific page. App code will need it. -mockProjectOptions = -> - - window.gl or= {} - window.gl.projectOptions = - 'gitlab-ce' : - issuesPath : projectIssuesPath - mrPath : projectMRsPath - projectName : projectName - - -mockGroupOptions = -> - - window.gl or= {} - window.gl.groupOptions = - 'gitlab-org' : - issuesPath : groupIssuesPath - mrPath : groupMRsPath - projectName : groupName - - -assertLinks = (list, issuesPath, mrsPath) -> - - issuesAssignedToMeLink = "#{issuesPath}/?assignee_id=#{userId}" - issuesIHaveCreatedLink = "#{issuesPath}/?author_id=#{userId}" - mrsAssignedToMeLink = "#{mrsPath}/?assignee_id=#{userId}" - mrsIHaveCreatedLink = "#{mrsPath}/?author_id=#{userId}" - - a1 = "a[href='#{issuesAssignedToMeLink}']" - a2 = "a[href='#{issuesIHaveCreatedLink}']" - a3 = "a[href='#{mrsAssignedToMeLink}']" - a4 = "a[href='#{mrsIHaveCreatedLink}']" - - expect(list.find(a1).length).toBe 1 - expect(list.find(a1).text()).toBe ' Issues assigned to me ' - - expect(list.find(a2).length).toBe 1 - expect(list.find(a2).text()).toBe " Issues I've created " - - expect(list.find(a3).length).toBe 1 - expect(list.find(a3).text()).toBe ' Merge requests assigned to me ' - - expect(list.find(a4).length).toBe 1 - expect(list.find(a4).text()).toBe " Merge requests I've created " - - -describe 'Search autocomplete dropdown', -> - - fixture.preload 'search_autocomplete.html' - - beforeEach -> - - fixture.load 'search_autocomplete.html' - widget = new SearchAutocomplete - - - it 'should show Dashboard specific dropdown menu', -> - - addBodyAttributes() - mockDashboardOptions() - widget.searchInput.focus() - - list = widget.wrap.find('.dropdown-menu').find 'ul' - assertLinks list, dashboardIssuesPath, dashboardMRsPath - - - it 'should show Group specific dropdown menu', -> - - addBodyAttributes 'group' - mockGroupOptions() - widget.searchInput.focus() - - list = widget.wrap.find('.dropdown-menu').find 'ul' - assertLinks list, groupIssuesPath, groupMRsPath - - - it 'should show Project specific dropdown menu', -> - - addBodyAttributes 'project' - mockProjectOptions() - widget.searchInput.focus() - - list = widget.wrap.find('.dropdown-menu').find 'ul' - assertLinks list, projectIssuesPath, projectMRsPath - - - it 'should not show category related menu if there is text in the input', -> - - addBodyAttributes 'project' - mockProjectOptions() - widget.searchInput.val 'help' - widget.searchInput.focus() - - list = widget.wrap.find('.dropdown-menu').find 'ul' - link = "a[href='#{projectIssuesPath}/?assignee_id=#{userId}']" - expect(list.find(link).length).toBe 0 diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js new file mode 100644 index 00000000000..7b6b55fe545 --- /dev/null +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -0,0 +1,74 @@ + +/*= require shortcuts_issuable */ + +(function() { + describe('ShortcutsIssuable', function() { + fixture.preload('issuable.html'); + beforeEach(function() { + fixture.load('issuable.html'); + return this.shortcut = new ShortcutsIssuable(); + }); + return describe('#replyWithSelectedText', function() { + var stubSelection; + stubSelection = function(text) { + return window.getSelection = function() { + return text; + }; + }; + beforeEach(function() { + return this.selector = 'form.js-main-target-form textarea#note_note'; + }); + describe('with empty selection', function() { + return it('does nothing', function() { + stubSelection(''); + this.shortcut.replyWithSelectedText(); + return expect($(this.selector).val()).toBe(''); + }); + }); + describe('with any selection', function() { + beforeEach(function() { + return stubSelection('Selected text.'); + }); + it('leaves existing input intact', function() { + $(this.selector).val('This text was already here.'); + expect($(this.selector).val()).toBe('This text was already here.'); + this.shortcut.replyWithSelectedText(); + return expect($(this.selector).val()).toBe("This text was already here.\n> Selected text.\n\n"); + }); + it('triggers `input`', function() { + var triggered; + triggered = false; + $(this.selector).on('input', function() { + return triggered = true; + }); + this.shortcut.replyWithSelectedText(); + return expect(triggered).toBe(true); + }); + return it('triggers `focus`', function() { + var focused; + focused = false; + $(this.selector).on('focus', function() { + return focused = true; + }); + this.shortcut.replyWithSelectedText(); + return expect(focused).toBe(true); + }); + }); + describe('with a one-line selection', function() { + return it('quotes the selection', function() { + stubSelection('This text has been selected.'); + this.shortcut.replyWithSelectedText(); + return expect($(this.selector).val()).toBe("> This text has been selected.\n\n"); + }); + }); + return describe('with a multi-line selection', function() { + return it('quotes the selected lines as a group', function() { + stubSelection("Selected line one.\n\nSelected line two.\nSelected line three.\n"); + this.shortcut.replyWithSelectedText(); + return expect($(this.selector).val()).toBe("> Selected line one.\n> Selected line two.\n> Selected line three.\n\n"); + }); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/shortcuts_issuable_spec.js.coffee b/spec/javascripts/shortcuts_issuable_spec.js.coffee deleted file mode 100644 index a01ad7140dd..00000000000 --- a/spec/javascripts/shortcuts_issuable_spec.js.coffee +++ /dev/null @@ -1,82 +0,0 @@ -#= require shortcuts_issuable - -describe 'ShortcutsIssuable', -> - fixture.preload('issuable.html') - - beforeEach -> - fixture.load('issuable.html') - @shortcut = new ShortcutsIssuable() - - describe '#replyWithSelectedText', -> - # Stub window.getSelection to return the provided String. - stubSelection = (text) -> - window.getSelection = -> text - - beforeEach -> - @selector = 'form.js-main-target-form textarea#note_note' - - describe 'with empty selection', -> - it 'does nothing', -> - stubSelection('') - @shortcut.replyWithSelectedText() - expect($(@selector).val()).toBe('') - - describe 'with any selection', -> - beforeEach -> - stubSelection('Selected text.') - - it 'leaves existing input intact', -> - $(@selector).val('This text was already here.') - expect($(@selector).val()).toBe('This text was already here.') - - @shortcut.replyWithSelectedText() - expect($(@selector).val()). - toBe("This text was already here.\n> Selected text.\n\n") - - it 'triggers `input`', -> - triggered = false - $(@selector).on 'input', -> triggered = true - @shortcut.replyWithSelectedText() - - expect(triggered).toBe(true) - - it 'triggers `focus`', -> - focused = false - $(@selector).on 'focus', -> focused = true - @shortcut.replyWithSelectedText() - - expect(focused).toBe(true) - - describe 'with a one-line selection', -> - it 'quotes the selection', -> - stubSelection('This text has been selected.') - - @shortcut.replyWithSelectedText() - - expect($(@selector).val()). - toBe("> This text has been selected.\n\n") - - describe 'with a multi-line selection', -> - it 'quotes the selected lines as a group', -> - stubSelection( - """ - Selected line one. - - Selected line two. - Selected line three. - - """ - ) - - @shortcut.replyWithSelectedText() - - expect($(@selector).val()). - toBe( - """ - > Selected line one. - > Selected line two. - > Selected line three. - - - """ - ) diff --git a/spec/javascripts/spec_helper.coffee b/spec/javascripts/spec_helper.coffee deleted file mode 100644 index 90b02a6aec5..00000000000 --- a/spec/javascripts/spec_helper.coffee +++ /dev/null @@ -1,47 +0,0 @@ -# PhantomJS (Teaspoons default driver) doesn't have support for -# Function.prototype.bind, which has caused confusion. Use this polyfill to -# avoid the confusion. - -#= require support/bind-poly - -# You can require your own javascript files here. By default this will include -# everything in application, however you may get better load performance if you -# require the specific files that are being used in the spec that tests them. - -#= require jquery -#= require jquery.turbolinks -#= require bootstrap -#= require underscore - -# Teaspoon includes some support files, but you can use anything from your own -# support path too. - -# require support/jasmine-jquery-1.7.0 -# require support/jasmine-jquery-2.0.0 -#= require support/jasmine-jquery-2.1.0 -# require support/sinon -# require support/your-support-file - -# Deferring execution - -# If you're using CommonJS, RequireJS or some other asynchronous library you can -# defer execution. Call Teaspoon.execute() after everything has been loaded. -# Simple example of a timeout: - -# Teaspoon.defer = true -# setTimeout(Teaspoon.execute, 1000) - -# Matching files - -# By default Teaspoon will look for files that match -# _spec.{js,js.coffee,.coffee}. Add a filename_spec.js file in your spec path -# and it'll be included in the default suite automatically. If you want to -# customize suites, check out the configuration in teaspoon_env.rb - -# Manifest - -# If you'd rather require your spec files manually (to control order for -# instance) you can disable the suite matcher in the configuration and use this -# file as a manifest. - -# For more information: http://github.com/modeset/teaspoon diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js new file mode 100644 index 00000000000..7d91ed0f855 --- /dev/null +++ b/spec/javascripts/spec_helper.js @@ -0,0 +1,22 @@ + +/*= require support/bind-poly */ + + +/*= require jquery */ + + +/*= require jquery.turbolinks */ + + +/*= require bootstrap */ + + +/*= require underscore */ + + +/*= require support/jasmine-jquery-2.1.0 */ + +(function() { + + +}).call(this); diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js new file mode 100644 index 00000000000..4e5dd1e59bf --- /dev/null +++ b/spec/javascripts/syntax_highlight_spec.js @@ -0,0 +1,44 @@ + +/*= require syntax_highlight */ + +(function() { + describe('Syntax Highlighter', function() { + var stubUserColorScheme; + stubUserColorScheme = function(value) { + if (window.gon == null) { + window.gon = {}; + } + return window.gon.user_color_scheme = value; + }; + describe('on a js-syntax-highlight element', function() { + beforeEach(function() { + return fixture.set('<div class="js-syntax-highlight"></div>'); + }); + return it('applies syntax highlighting', function() { + stubUserColorScheme('monokai'); + $('.js-syntax-highlight').syntaxHighlight(); + return expect($('.js-syntax-highlight')).toHaveClass('monokai'); + }); + }); + return describe('on a parent element', function() { + beforeEach(function() { + return fixture.set("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>"); + }); + it('applies highlighting to all applicable children', function() { + stubUserColorScheme('monokai'); + $('.parent').syntaxHighlight(); + expect($('.parent, .foo')).not.toHaveClass('monokai'); + return expect($('.monokai').length).toBe(2); + }); + return it('prevents an infinite loop when no matches exist', function() { + var highlight; + fixture.set('<div></div>'); + highlight = function() { + return $('div').syntaxHighlight(); + }; + return expect(highlight).not.toThrow(); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/syntax_highlight_spec.js.coffee b/spec/javascripts/syntax_highlight_spec.js.coffee deleted file mode 100644 index 6a73b6bf32c..00000000000 --- a/spec/javascripts/syntax_highlight_spec.js.coffee +++ /dev/null @@ -1,42 +0,0 @@ -#= require syntax_highlight - -describe 'Syntax Highlighter', -> - stubUserColorScheme = (value) -> - window.gon ?= {} - window.gon.user_color_scheme = value - - describe 'on a js-syntax-highlight element', -> - beforeEach -> - fixture.set('<div class="js-syntax-highlight"></div>') - - it 'applies syntax highlighting', -> - stubUserColorScheme('monokai') - - $('.js-syntax-highlight').syntaxHighlight() - - expect($('.js-syntax-highlight')).toHaveClass('monokai') - - describe 'on a parent element', -> - beforeEach -> - fixture.set """ - <div class="parent"> - <div class="js-syntax-highlight"></div> - <div class="foo"></div> - <div class="js-syntax-highlight"></div> - </div> - """ - - it 'applies highlighting to all applicable children', -> - stubUserColorScheme('monokai') - - $('.parent').syntaxHighlight() - - expect($('.parent, .foo')).not.toHaveClass('monokai') - expect($('.monokai').length).toBe(2) - - it 'prevents an infinite loop when no matches exist', -> - fixture.set('<div></div>') - - highlight = -> $('div').syntaxHighlight() - - expect(highlight).not.toThrow() diff --git a/spec/javascripts/u2f/authenticate_spec.coffee b/spec/javascripts/u2f/authenticate_spec.coffee deleted file mode 100644 index 8ffeda11704..00000000000 --- a/spec/javascripts/u2f/authenticate_spec.coffee +++ /dev/null @@ -1,51 +0,0 @@ -#= require u2f/authenticate -#= require u2f/util -#= require u2f/error -#= require u2f -#= require ./mock_u2f_device - -describe 'U2FAuthenticate', -> - fixture.load('u2f/authenticate') - - beforeEach -> - @u2fDevice = new MockU2FDevice - @container = $("#js-authenticate-u2f") - @component = new U2FAuthenticate(@container, {sign_requests: []}, "token") - @component.start() - - it 'allows authenticating via a U2F device', -> - setupButton = @container.find("#js-login-u2f-device") - setupMessage = @container.find("p") - expect(setupMessage.text()).toContain('Insert your security key') - expect(setupButton.text()).toBe('Login Via U2F Device') - setupButton.trigger('click') - - inProgressMessage = @container.find("p") - expect(inProgressMessage.text()).toContain("Trying to communicate with your device") - - @u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"}) - authenticatedMessage = @container.find("p") - deviceResponse = @container.find('#js-device-response') - expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server") - expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}') - - describe "errors", -> - it "displays an error message", -> - setupButton = @container.find("#js-login-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToAuthenticateRequest({errorCode: "error!"}) - errorMessage = @container.find("p") - expect(errorMessage.text()).toContain("There was a problem communicating with your device") - - it "allows retrying authentication after an error", -> - setupButton = @container.find("#js-login-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToAuthenticateRequest({errorCode: "error!"}) - retryButton = @container.find("#js-u2f-try-again") - retryButton.trigger('click') - - setupButton = @container.find("#js-login-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"}) - authenticatedMessage = @container.find("p") - expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server") diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js new file mode 100644 index 00000000000..e008ce956ad --- /dev/null +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -0,0 +1,75 @@ + +/*= require u2f/authenticate */ + + +/*= require u2f/util */ + + +/*= require u2f/error */ + + +/*= require u2f */ + + +/*= require ./mock_u2f_device */ + +(function() { + describe('U2FAuthenticate', function() { + fixture.load('u2f/authenticate'); + beforeEach(function() { + this.u2fDevice = new MockU2FDevice; + this.container = $("#js-authenticate-u2f"); + this.component = new U2FAuthenticate(this.container, { + sign_requests: [] + }, "token"); + return this.component.start(); + }); + it('allows authenticating via a U2F device', function() { + var authenticatedMessage, deviceResponse, inProgressMessage, setupButton, setupMessage; + setupButton = this.container.find("#js-login-u2f-device"); + setupMessage = this.container.find("p"); + expect(setupMessage.text()).toContain('Insert your security key'); + expect(setupButton.text()).toBe('Login Via U2F Device'); + setupButton.trigger('click'); + inProgressMessage = this.container.find("p"); + expect(inProgressMessage.text()).toContain("Trying to communicate with your device"); + this.u2fDevice.respondToAuthenticateRequest({ + deviceData: "this is data from the device" + }); + authenticatedMessage = this.container.find("p"); + deviceResponse = this.container.find('#js-device-response'); + expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server"); + return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}'); + }); + return describe("errors", function() { + it("displays an error message", function() { + var errorMessage, setupButton; + setupButton = this.container.find("#js-login-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToAuthenticateRequest({ + errorCode: "error!" + }); + errorMessage = this.container.find("p"); + return expect(errorMessage.text()).toContain("There was a problem communicating with your device"); + }); + return it("allows retrying authentication after an error", function() { + var authenticatedMessage, retryButton, setupButton; + setupButton = this.container.find("#js-login-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToAuthenticateRequest({ + errorCode: "error!" + }); + retryButton = this.container.find("#js-u2f-try-again"); + retryButton.trigger('click'); + setupButton = this.container.find("#js-login-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToAuthenticateRequest({ + deviceData: "this is data from the device" + }); + authenticatedMessage = this.container.find("p"); + return expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server"); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js new file mode 100644 index 00000000000..ca91a716ba3 --- /dev/null +++ b/spec/javascripts/u2f/mock_u2f_device.js @@ -0,0 +1,33 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.MockU2FDevice = (function() { + function MockU2FDevice() { + this.respondToAuthenticateRequest = bind(this.respondToAuthenticateRequest, this); + this.respondToRegisterRequest = bind(this.respondToRegisterRequest, this); + window.u2f || (window.u2f = {}); + window.u2f.register = (function(_this) { + return function(appId, registerRequests, signRequests, callback) { + return _this.registerCallback = callback; + }; + })(this); + window.u2f.sign = (function(_this) { + return function(appId, challenges, signRequests, callback) { + return _this.authenticateCallback = callback; + }; + })(this); + } + + MockU2FDevice.prototype.respondToRegisterRequest = function(params) { + return this.registerCallback(params); + }; + + MockU2FDevice.prototype.respondToAuthenticateRequest = function(params) { + return this.authenticateCallback(params); + }; + + return MockU2FDevice; + + })(); + +}).call(this); diff --git a/spec/javascripts/u2f/mock_u2f_device.js.coffee b/spec/javascripts/u2f/mock_u2f_device.js.coffee deleted file mode 100644 index 97ed0e83a0e..00000000000 --- a/spec/javascripts/u2f/mock_u2f_device.js.coffee +++ /dev/null @@ -1,15 +0,0 @@ -class @MockU2FDevice - constructor: () -> - window.u2f ||= {} - - window.u2f.register = (appId, registerRequests, signRequests, callback) => - @registerCallback = callback - - window.u2f.sign = (appId, challenges, signRequests, callback) => - @authenticateCallback = callback - - respondToRegisterRequest: (params) => - @registerCallback(params) - - respondToAuthenticateRequest: (params) => - @authenticateCallback(params) diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js new file mode 100644 index 00000000000..21c5266c60e --- /dev/null +++ b/spec/javascripts/u2f/register_spec.js @@ -0,0 +1,81 @@ + +/*= require u2f/register */ + + +/*= require u2f/util */ + + +/*= require u2f/error */ + + +/*= require u2f */ + + +/*= require ./mock_u2f_device */ + +(function() { + describe('U2FRegister', function() { + fixture.load('u2f/register'); + beforeEach(function() { + this.u2fDevice = new MockU2FDevice; + this.container = $("#js-register-u2f"); + this.component = new U2FRegister(this.container, $("#js-register-u2f-templates"), {}, "token"); + return this.component.start(); + }); + it('allows registering a U2F device', function() { + var deviceResponse, inProgressMessage, registeredMessage, setupButton; + setupButton = this.container.find("#js-setup-u2f-device"); + expect(setupButton.text()).toBe('Setup New U2F Device'); + setupButton.trigger('click'); + inProgressMessage = this.container.children("p"); + expect(inProgressMessage.text()).toContain("Trying to communicate with your device"); + this.u2fDevice.respondToRegisterRequest({ + deviceData: "this is data from the device" + }); + registeredMessage = this.container.find('p'); + deviceResponse = this.container.find('#js-device-response'); + expect(registeredMessage.text()).toContain("Your device was successfully set up!"); + return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}'); + }); + return describe("errors", function() { + it("doesn't allow the same device to be registered twice (for the same user", function() { + var errorMessage, setupButton; + setupButton = this.container.find("#js-setup-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToRegisterRequest({ + errorCode: 4 + }); + errorMessage = this.container.find("p"); + return expect(errorMessage.text()).toContain("already been registered with us"); + }); + it("displays an error message for other errors", function() { + var errorMessage, setupButton; + setupButton = this.container.find("#js-setup-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToRegisterRequest({ + errorCode: "error!" + }); + errorMessage = this.container.find("p"); + return expect(errorMessage.text()).toContain("There was a problem communicating with your device"); + }); + return it("allows retrying registration after an error", function() { + var registeredMessage, retryButton, setupButton; + setupButton = this.container.find("#js-setup-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToRegisterRequest({ + errorCode: "error!" + }); + retryButton = this.container.find("#U2FTryAgain"); + retryButton.trigger('click'); + setupButton = this.container.find("#js-setup-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToRegisterRequest({ + deviceData: "this is data from the device" + }); + registeredMessage = this.container.find("p"); + return expect(registeredMessage.text()).toContain("Your device was successfully set up!"); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/u2f/register_spec.js.coffee b/spec/javascripts/u2f/register_spec.js.coffee deleted file mode 100644 index 87dc769792b..00000000000 --- a/spec/javascripts/u2f/register_spec.js.coffee +++ /dev/null @@ -1,56 +0,0 @@ -#= require u2f/register -#= require u2f/util -#= require u2f/error -#= require u2f -#= require ./mock_u2f_device - -describe 'U2FRegister', -> - fixture.load('u2f/register') - - beforeEach -> - @u2fDevice = new MockU2FDevice - @container = $("#js-register-u2f") - @component = new U2FRegister(@container, $("#js-register-u2f-templates"), {}, "token") - @component.start() - - it 'allows registering a U2F device', -> - setupButton = @container.find("#js-setup-u2f-device") - expect(setupButton.text()).toBe('Setup New U2F Device') - setupButton.trigger('click') - - inProgressMessage = @container.children("p") - expect(inProgressMessage.text()).toContain("Trying to communicate with your device") - - @u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"}) - registeredMessage = @container.find('p') - deviceResponse = @container.find('#js-device-response') - expect(registeredMessage.text()).toContain("Your device was successfully set up!") - expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}') - - describe "errors", -> - it "doesn't allow the same device to be registered twice (for the same user", -> - setupButton = @container.find("#js-setup-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToRegisterRequest({errorCode: 4}) - errorMessage = @container.find("p") - expect(errorMessage.text()).toContain("already been registered with us") - - it "displays an error message for other errors", -> - setupButton = @container.find("#js-setup-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToRegisterRequest({errorCode: "error!"}) - errorMessage = @container.find("p") - expect(errorMessage.text()).toContain("There was a problem communicating with your device") - - it "allows retrying registration after an error", -> - setupButton = @container.find("#js-setup-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToRegisterRequest({errorCode: "error!"}) - retryButton = @container.find("#U2FTryAgain") - retryButton.trigger('click') - - setupButton = @container.find("#js-setup-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"}) - registeredMessage = @container.find("p") - expect(registeredMessage.text()).toContain("Your device was successfully set up!") diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js new file mode 100644 index 00000000000..3d680ec8ea3 --- /dev/null +++ b/spec/javascripts/zen_mode_spec.js @@ -0,0 +1,73 @@ + +/*= require zen_mode */ + +(function() { + var enterZen, escapeKeydown, exitZen; + + describe('ZenMode', function() { + fixture.preload('zen_mode.html'); + beforeEach(function() { + fixture.load('zen_mode.html'); + spyOn(Dropzone, 'forElement').and.callFake(function() { + return { + enable: function() { + return true; + } + }; + }); + this.zen = new ZenMode(); + return this.zen.scroll_position = 456; + }); + describe('on enter', function() { + it('pauses Mousetrap', function() { + spyOn(Mousetrap, 'pause'); + enterZen(); + return expect(Mousetrap.pause).toHaveBeenCalled(); + }); + return it('removes textarea styling', function() { + $('textarea').attr('style', 'height: 400px'); + enterZen(); + return expect('textarea').not.toHaveAttr('style'); + }); + }); + describe('in use', function() { + beforeEach(function() { + return enterZen(); + }); + return it('exits on Escape', function() { + escapeKeydown(); + return expect($('.zen-backdrop')).not.toHaveClass('fullscreen'); + }); + }); + return describe('on exit', function() { + beforeEach(function() { + return enterZen(); + }); + it('unpauses Mousetrap', function() { + spyOn(Mousetrap, 'unpause'); + exitZen(); + return expect(Mousetrap.unpause).toHaveBeenCalled(); + }); + return it('restores the scroll position', function() { + spyOn(this.zen, 'scrollTo'); + exitZen(); + return expect(this.zen.scrollTo).toHaveBeenCalled(); + }); + }); + }); + + enterZen = function() { + return $('a.js-zen-enter').click(); + }; + + exitZen = function() { + return $('a.js-zen-leave').click(); + }; + + escapeKeydown = function() { + return $('textarea').trigger($.Event('keydown', { + keyCode: 27 + })); + }; + +}).call(this); diff --git a/spec/javascripts/zen_mode_spec.js.coffee b/spec/javascripts/zen_mode_spec.js.coffee deleted file mode 100644 index b790fce01ed..00000000000 --- a/spec/javascripts/zen_mode_spec.js.coffee +++ /dev/null @@ -1,51 +0,0 @@ -#= require zen_mode - -describe 'ZenMode', -> - fixture.preload('zen_mode.html') - - beforeEach -> - fixture.load('zen_mode.html') - - # Stub Dropzone.forElement(...).enable() - spyOn(Dropzone, 'forElement').and.callFake -> - enable: -> true - - @zen = new ZenMode() - - # Set this manually because we can't actually scroll the window - @zen.scroll_position = 456 - - describe 'on enter', -> - it 'pauses Mousetrap', -> - spyOn(Mousetrap, 'pause') - enterZen() - expect(Mousetrap.pause).toHaveBeenCalled() - - it 'removes textarea styling', -> - $('textarea').attr('style', 'height: 400px') - enterZen() - expect('textarea').not.toHaveAttr('style') - - describe 'in use', -> - beforeEach -> enterZen() - - it 'exits on Escape', -> - escapeKeydown() - expect($('.zen-backdrop')).not.toHaveClass('fullscreen') - - describe 'on exit', -> - beforeEach -> enterZen() - - it 'unpauses Mousetrap', -> - spyOn(Mousetrap, 'unpause') - exitZen() - expect(Mousetrap.unpause).toHaveBeenCalled() - - it 'restores the scroll position', -> - spyOn(@zen, 'scrollTo') - exitZen() - expect(@zen.scrollTo).toHaveBeenCalled() - -enterZen = -> $('a.js-zen-enter').click() # Ohmmmmmmm -exitZen = -> $('a.js-zen-leave').click() -escapeKeydown = -> $('textarea').trigger($.Event('keydown', {keyCode: 27})) diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index b9e4a4eaf0e..9921171f2aa 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -1,5 +1,3 @@ -# encoding: UTF-8 - require 'spec_helper' describe Banzai::Filter::RelativeLinkFilter, lib: true do @@ -19,6 +17,10 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do %(<img src="#{path}" />) end + def video(path) + %(<video src="#{path}"></video>) + end + def link(path) %(<a href="#{path}">#{path}</a>) end @@ -39,6 +41,12 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do doc = filter(image('files/images/logo-black.png')) expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png' end + + it 'does not modify any relative URL in video' do + doc = filter(video('files/videos/intro.mp4'), commit: project.commit('video'), ref: 'video') + + expect(doc.at_css('video')['src']).to eq 'files/videos/intro.mp4' + end end shared_examples :relative_to_requested do @@ -113,11 +121,26 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do end it 'rebuilds relative URL for an image in the repo' do + doc = filter(image('files/images/logo-black.png')) + + expect(doc.at_css('img')['src']). + to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png" + end + + it 'rebuilds relative URL for link to an image in the repo' do doc = filter(link('files/images/logo-black.png')) + expect(doc.at_css('a')['href']). to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png" end + it 'rebuilds relative URL for a video in the repo' do + doc = filter(video('files/videos/intro.mp4'), commit: project.commit('video'), ref: 'video') + + expect(doc.at_css('video')['src']). + to eq "/#{project_path}/raw/video/files/videos/intro.mp4" + end + it 'does not modify relative URL with an anchor only' do doc = filter(link('#section-1')) expect(doc.at_css('a')['href']).to eq '#section-1' diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb index 6a5d003e87f..356dd01a03a 100644 --- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb +++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb @@ -1,5 +1,3 @@ -# encoding: UTF-8 - require 'spec_helper' describe Banzai::Filter::TableOfContentsFilter, lib: true do diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb index 273d2ed709a..8b76c1d73c9 100644 --- a/spec/lib/banzai/filter/upload_link_filter_spec.rb +++ b/spec/lib/banzai/filter/upload_link_filter_spec.rb @@ -1,5 +1,3 @@ -# encoding: UTF-8 - require 'spec_helper' describe Banzai::Filter::UploadLinkFilter, lib: true do diff --git a/spec/lib/banzai/filter/video_link_filter_spec.rb b/spec/lib/banzai/filter/video_link_filter_spec.rb new file mode 100644 index 00000000000..cc4349f80ba --- /dev/null +++ b/spec/lib/banzai/filter/video_link_filter_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe Banzai::Filter::VideoLinkFilter, lib: true do + def filter(doc, contexts = {}) + contexts.reverse_merge!({ + project: project + }) + + described_class.call(doc, contexts) + end + + def link_to_image(path) + %(<img src="#{path}" />) + end + + let(:project) { create(:project) } + + context 'when the element src has a video extension' do + UploaderHelper::VIDEO_EXT.each do |ext| + it "replaces the image tag 'path/video.#{ext}' with a video tag" do + container = filter(link_to_image("/path/video.#{ext}")).children.first + + expect(container.name).to eq 'div' + expect(container['class']).to eq 'video-container' + + video, paragraph = container.children + + expect(video.name).to eq 'video' + expect(video['src']).to eq "/path/video.#{ext}" + + expect(paragraph.name).to eq 'p' + + link = paragraph.children.first + + expect(link.name).to eq 'a' + expect(link['href']).to eq "/path/video.#{ext}" + expect(link['target']).to eq '_blank' + end + end + end + + context 'when the element src is an image' do + it 'leaves the document unchanged' do + element = filter(link_to_image('/path/my_image.jpg')).children.first + + expect(element.name).to eq 'img' + expect(element['src']).to eq '/path/my_image.jpg' + end + end + +end diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb index 5f76b70c6f5..2aa5ae44f54 100644 --- a/spec/lib/gitlab/diff/parallel_diff_spec.rb +++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb @@ -11,11 +11,51 @@ describe Gitlab::Diff::ParallelDiff, lib: true do let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: repository) } subject { described_class.new(diff_file) } - let(:parallel_diff_result_array) { YAML.load_file("#{Rails.root}/spec/fixtures/parallel_diff_result.yml") } - describe '#parallelize' do it 'should return an array of arrays containing the parsed diff' do - expect(subject.parallelize).to match_array(parallel_diff_result_array) + diff_lines = diff_file.highlighted_diff_lines + expected = [ + # Unchanged lines + { left: diff_lines[0], right: diff_lines[0] }, + { left: diff_lines[1], right: diff_lines[1] }, + { left: diff_lines[2], right: diff_lines[2] }, + { left: diff_lines[3], right: diff_lines[3] }, + { left: diff_lines[4], right: diff_lines[5] }, + { left: diff_lines[6], right: diff_lines[6] }, + { left: diff_lines[7], right: diff_lines[7] }, + { left: diff_lines[8], right: diff_lines[8] }, + + # Changed lines + { left: diff_lines[9], right: diff_lines[11] }, + { left: diff_lines[10], right: diff_lines[12] }, + + # Added lines + { left: nil, right: diff_lines[13] }, + { left: nil, right: diff_lines[14] }, + { left: nil, right: diff_lines[15] }, + { left: nil, right: diff_lines[16] }, + { left: nil, right: diff_lines[17] }, + { left: nil, right: diff_lines[18] }, + + # Unchanged lines + { left: diff_lines[19], right: diff_lines[19] }, + { left: diff_lines[20], right: diff_lines[20] }, + { left: diff_lines[21], right: diff_lines[21] }, + { left: diff_lines[22], right: diff_lines[22] }, + { left: diff_lines[23], right: diff_lines[23] }, + { left: diff_lines[24], right: diff_lines[24] }, + { left: diff_lines[25], right: diff_lines[25] }, + + # Added line + { left: nil, right: diff_lines[26] }, + + # Unchanged lines + { left: diff_lines[27], right: diff_lines[27] }, + { left: diff_lines[28], right: diff_lines[28] }, + { left: diff_lines[29], right: diff_lines[29] } + ] + + expect(subject.parallelize).to eq(expected) end end end diff --git a/spec/lib/gitlab/email/attachment_uploader_spec.rb b/spec/lib/gitlab/email/attachment_uploader_spec.rb index 476a21bf996..08b2577ecc4 100644 --- a/spec/lib/gitlab/email/attachment_uploader_spec.rb +++ b/spec/lib/gitlab/email/attachment_uploader_spec.rb @@ -11,7 +11,6 @@ describe Gitlab::Email::AttachmentUploader, lib: true do link = links.first expect(link).not_to be_nil - expect(link[:is_image]).to be_truthy expect(link[:alt]).to eq("bricks") expect(link[:url]).to include("bricks.png") end diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb index 36267faeb93..84d2584a791 100644 --- a/spec/lib/gitlab/email/receiver_spec.rb +++ b/spec/lib/gitlab/email/receiver_spec.rb @@ -115,7 +115,6 @@ describe Gitlab::Email::Receiver, lib: true do [ { url: "uploads/image.png", - is_image: true, alt: "image", markdown: markdown } diff --git a/spec/lib/repository_cache_spec.rb b/spec/lib/repository_cache_spec.rb index 63b5292b098..f227926f39c 100644 --- a/spec/lib/repository_cache_spec.rb +++ b/spec/lib/repository_cache_spec.rb @@ -1,33 +1,34 @@ -require_relative '../../lib/repository_cache' +require 'spec_helper' describe RepositoryCache, lib: true do + let(:project) { create(:project) } let(:backend) { double('backend').as_null_object } - let(:cache) { RepositoryCache.new('example', backend) } + let(:cache) { RepositoryCache.new('example', project.id, backend) } describe '#cache_key' do it 'includes the namespace' do - expect(cache.cache_key(:foo)).to eq 'foo:example' + expect(cache.cache_key(:foo)).to eq "foo:example:#{project.id}" end end describe '#expire' do it 'expires the given key from the cache' do cache.expire(:foo) - expect(backend).to have_received(:delete).with('foo:example') + expect(backend).to have_received(:delete).with("foo:example:#{project.id}") end end describe '#fetch' do it 'fetches the given key from the cache' do cache.fetch(:bar) - expect(backend).to have_received(:fetch).with('bar:example') + expect(backend).to have_received(:fetch).with("bar:example:#{project.id}") end it 'accepts a block' do p = -> {} cache.fetch(:baz, &p) - expect(backend).to have_received(:fetch).with('baz:example', &p) + expect(backend).to have_received(:fetch).with("baz:example:#{project.id}", &p) end end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 0a9b10bebea..3685b2b17b5 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -12,7 +12,7 @@ describe Notify do context 'for a project' do describe 'items that are assignable, the email' do let(:current_user) { create(:user, email: "current@email.com") } - let(:assignee) { create(:user, email: 'assignee@example.com') } + let(:assignee) { create(:user, email: 'assignee@example.com', name: 'John Doe') } let(:previous_assignee) { create(:user, name: 'Previous Assignee') } shared_examples 'an assignee email' do diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 1acb5846fcf..cd5f40fe3d2 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -1,6 +1,62 @@ require 'spec_helper' describe Ability, lib: true do + describe '.can_edit_note?' do + let(:project) { create(:empty_project) } + let!(:note) { create(:note_on_issue, project: project) } + + context 'using an anonymous user' do + it 'returns false' do + expect(described_class.can_edit_note?(nil, note)).to be_falsy + end + end + + context 'using a system note' do + it 'returns false' do + system_note = create(:note, system: true) + user = create(:user) + + expect(described_class.can_edit_note?(user, system_note)).to be_falsy + end + end + + context 'using users with different access levels' do + let(:user) { create(:user) } + + it 'returns true for the author' do + expect(described_class.can_edit_note?(note.author, note)).to be_truthy + end + + it 'returns false for a guest user' do + project.team << [user, :guest] + + expect(described_class.can_edit_note?(user, note)).to be_falsy + end + + it 'returns false for a developer' do + project.team << [user, :developer] + + expect(described_class.can_edit_note?(user, note)).to be_falsy + end + + it 'returns true for a master' do + project.team << [user, :master] + + expect(described_class.can_edit_note?(user, note)).to be_truthy + end + + it 'returns true for a group owner' do + group = create(:group) + project.project_group_links.create( + group: group, + group_access: Gitlab::Access::MASTER) + group.add_owner(user) + + expect(described_class.can_edit_note?(user, note)).to be_truthy + end + end + end + describe '.users_that_can_read_project' do context 'using a public project' do it 'returns all the users' do diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb index 78e95c8fac5..1e5d6a34f83 100644 --- a/spec/models/blob_spec.rb +++ b/spec/models/blob_spec.rb @@ -33,6 +33,22 @@ describe Blob do end end + describe '#video?' do + it 'is falsey with image extension' do + git_blob = Gitlab::Git::Blob.new(name: 'image.png') + + expect(described_class.decorate(git_blob)).not_to be_video + end + + UploaderHelper::VIDEO_EXT.each do |ext| + it "is truthy when extension is .#{ext}" do + git_blob = Gitlab::Git::Blob.new(name: "video.#{ext}") + + expect(described_class.decorate(git_blob)).to be_video + end + end + end + describe '#to_partial_path' do def stubbed_blob(overrides = {}) overrides.reverse_merge!( diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 2de305738cc..dc88697199b 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -5,7 +5,9 @@ describe Ci::Build, models: true do let(:pipeline) do create(:ci_pipeline, project: project, - sha: project.commit.id) + sha: project.commit.id, + ref: project.default_branch, + status: 'success') end let(:build) { create(:ci_build, pipeline: pipeline) } @@ -257,7 +259,7 @@ describe Ci::Build, models: true do let(:trigger) { create(:ci_trigger, project: project) } let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) } let(:user_trigger_variable) do - { key: :TRIGGER_KEY, value: 'TRIGGER_VALUE', public: false } + { key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false } end let(:predefined_trigger_variable) do { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } @@ -720,7 +722,7 @@ describe Ci::Build, models: true do describe '#erasable?' do subject { build.erasable? } - it { is_expected.to eq true } + it { is_expected.to be_truthy } end describe '#erased?' do @@ -728,7 +730,7 @@ describe Ci::Build, models: true do subject { build.erased? } context 'build has not been erased' do - it { is_expected.to be false } + it { is_expected.to be_falsey } end context 'build has been erased' do @@ -736,12 +738,13 @@ describe Ci::Build, models: true do build.erase end - it { is_expected.to be true } + it { is_expected.to be_truthy } end end context 'metadata and build trace are not available' do let!(:build) { create(:ci_build, :success, :artifacts) } + before do build.remove_artifacts_metadata! end @@ -763,19 +766,19 @@ describe Ci::Build, models: true do describe '#retryable?' do context 'when build is running' do - before { build.run! } - - it 'should return false' do - expect(build.retryable?).to be false + before do + build.run! end + + it { expect(build).not_to be_retryable } end context 'when build is finished' do - before { build.success! } - - it 'should return true' do - expect(build.retryable?).to be true + before do + build.success! end + + it { expect(build).to be_retryable } end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index ba02d5fe977..ec1544bf815 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -212,6 +212,7 @@ eos it 'returns the URI type at the given path' do expect(commit.uri_type('files/html')).to be(:tree) expect(commit.uri_type('files/images/logo-black.png')).to be(:raw) + expect(project.commit('video').uri_type('files/videos/intro.mp4')).to be(:raw) expect(commit.uri_type('files/js/application.js')).to be(:blob) end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 40181a8b906..44cd3c08718 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -79,6 +79,18 @@ describe Member, models: true do @accepted_request_member = project.requesters.find_by(user_id: accepted_request_user.id).tap { |m| m.accept_request } end + describe '.access_for_user_ids' do + it 'returns the right access levels' do + users = [@owner_user.id, @master_user.id] + expected = { + @owner_user.id => Gitlab::Access::OWNER, + @master_user.id => Gitlab::Access::MASTER + } + + expect(described_class.access_for_user_ids(users)).to eq(expected) + end + end + describe '.invite' do it { expect(described_class.invite).not_to include @master } it { expect(described_class.invite).to include @invited_member } diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 7d0697dab42..1243f5420a7 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -135,22 +135,30 @@ describe Note, models: true do let!(:note2) { create(:note_on_issue) } it "reads the rendered note body from the cache" do - expect(Banzai::Renderer).to receive(:render). - with(note1.note, - pipeline: :note, - cache_key: [note1, "note"], - project: note1.project, - author: note1.author) - - expect(Banzai::Renderer).to receive(:render). - with(note2.note, - pipeline: :note, - cache_key: [note2, "note"], - project: note2.project, - author: note2.author) - - note1.all_references - note2.all_references + expect(Banzai::Renderer).to receive(:cache_collection_render). + with([{ + text: note1.note, + context: { + pipeline: :note, + cache_key: [note1, "note"], + project: note1.project, + author: note1.author + } + }]).and_call_original + + expect(Banzai::Renderer).to receive(:cache_collection_render). + with([{ + text: note2.note, + context: { + pipeline: :note, + cache_key: [note2, "note"], + project: note2.project, + author: note2.author + } + }]).and_call_original + + note1.all_references.users + note2.all_references.users end end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 5f618322aab..62ae5f6cf74 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -340,18 +340,36 @@ describe HipchatService, models: true do end context "#message_options" do - it "should be set to the defaults" do - expect(hipchat.send(:message_options)).to eq({ notify: false, color: 'yellow' }) + it "is set to the defaults" do + expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'yellow' }) end - it "should set notfiy to true" do + it "sets notify to true" do allow(hipchat).to receive(:notify).and_return('1') - expect(hipchat.send(:message_options)).to eq({ notify: true, color: 'yellow' }) + + expect(hipchat.__send__(:message_options)).to eq({ notify: true, color: 'yellow' }) end - it "should set the color" do + it "sets the color" do allow(hipchat).to receive(:color).and_return('red') - expect(hipchat.send(:message_options)).to eq({ notify: false, color: 'red' }) + + expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'red' }) + end + + context 'with a successful build' do + it 'uses the green color' do + build_data = { object_kind: 'build', commit: { status: 'success' } } + + expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'green' }) + end + end + + context 'with a failed build' do + it 'uses the red color' do + build_data = { object_kind: 'build', commit: { status: 'failed' } } + + expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'red' }) + end end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index a2b089c51e2..c884bb31199 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -377,7 +377,7 @@ describe Project, models: true do describe '#repository' do let(:project) { create(:project) } - it 'should return valid repo' do + it 'returns valid repo' do expect(project.repository).to be_kind_of(Repository) end end @@ -458,44 +458,54 @@ describe Project, models: true do end end - describe "#cache_has_external_wiki" do + describe '#external_wiki' do let(:project) { create(:project) } - it "stores true if there is an external wiki" do - services = double(:service, external_wikis: [ExternalWikiService.new]) - expect(project).to receive(:services).and_return(services) + context 'with an active external wiki' do + before do + create(:service, project: project, type: 'ExternalWikiService', active: true) + project.external_wiki + end - expect do - project.cache_has_external_wiki - end.to change { project.has_external_wiki }.to(true) - end + it 'sets :has_external_wiki as true' do + expect(project.has_external_wiki).to be(true) + end - it "stores false if there is no external wiki" do - services = double(:service, external_wikis: []) - expect(project).to receive(:services).and_return(services) + it 'sets :has_external_wiki as false if an external wiki service is destroyed later' do + expect(project.has_external_wiki).to be(true) - expect do - project.cache_has_external_wiki - end.to change { project.has_external_wiki }.to(false) + project.services.external_wikis.first.destroy + + expect(project.has_external_wiki).to be(false) + end end - it "changes to true if an external wiki service is created later" do - expect do - project.cache_has_external_wiki - end.to change { project.has_external_wiki }.to(false) + context 'with an inactive external wiki' do + before do + create(:service, project: project, type: 'ExternalWikiService', active: false) + end - expect do - create(:service, type: "ExternalWikiService", project: project) - end.to change { project.has_external_wiki }.to(true) + it 'sets :has_external_wiki as false' do + expect(project.has_external_wiki).to be(false) + end end - it "changes to false if an external wiki service is destroyed later" do - service = create(:service, type: "ExternalWikiService", project: project) - expect(project.has_external_wiki).to be_truthy + context 'with no external wiki' do + before do + project.external_wiki + end - expect do - service.destroy - end.to change { project.has_external_wiki }.to(false) + it 'sets :has_external_wiki as false' do + expect(project.has_external_wiki).to be(false) + end + + it 'sets :has_external_wiki as true if an external wiki service is created later' do + expect(project.has_external_wiki).to be(false) + + create(:service, project: project, type: 'ExternalWikiService', active: true) + + expect(project.has_external_wiki).to be(true) + end end end @@ -1155,6 +1165,111 @@ describe Project, models: true do end end + describe '#latest_successful_builds_for' do + def create_pipeline(status = 'success') + create(:ci_pipeline, project: project, + sha: project.commit.sha, + ref: project.default_branch, + status: status) + end + + def create_build(new_pipeline = pipeline, name = 'test') + create(:ci_build, :success, :artifacts, + pipeline: new_pipeline, + status: new_pipeline.status, + name: name) + end + + let(:project) { create(:project) } + let(:pipeline) { create_pipeline } + + context 'with many builds' do + it 'gives the latest builds from latest pipeline' do + pipeline1 = create_pipeline + pipeline2 = create_pipeline + build1_p2 = create_build(pipeline2, 'test') + create_build(pipeline1, 'test') + create_build(pipeline1, 'test2') + build2_p2 = create_build(pipeline2, 'test2') + + latest_builds = project.latest_successful_builds_for + + expect(latest_builds).to contain_exactly(build2_p2, build1_p2) + end + end + + context 'with succeeded pipeline' do + let!(:build) { create_build } + + context 'standalone pipeline' do + it 'returns builds for ref for default_branch' do + builds = project.latest_successful_builds_for + + expect(builds).to contain_exactly(build) + end + + it 'returns empty relation if the build cannot be found' do + builds = project.latest_successful_builds_for('TAIL') + + expect(builds).to be_kind_of(ActiveRecord::Relation) + expect(builds).to be_empty + end + end + + context 'with some pending pipeline' do + before do + create_build(create_pipeline('pending')) + end + + it 'gives the latest build from latest pipeline' do + latest_build = project.latest_successful_builds_for + + expect(latest_build).to contain_exactly(build) + end + end + end + + context 'with pending pipeline' do + before do + pipeline.update(status: 'pending') + create_build(pipeline) + end + + it 'returns empty relation' do + builds = project.latest_successful_builds_for + + expect(builds).to be_kind_of(ActiveRecord::Relation) + expect(builds).to be_empty + end + end + end + + describe '#add_import_job' do + context 'forked' do + let(:forked_project_link) { create(:forked_project_link) } + let(:forked_from_project) { forked_project_link.forked_from_project } + let(:project) { forked_project_link.forked_to_project } + + it 'schedules a RepositoryForkWorker job' do + expect(RepositoryForkWorker).to receive(:perform_async). + with(project.id, forked_from_project.repository_storage_path, + forked_from_project.path_with_namespace, project.namespace.path) + + project.add_import_job + end + end + + context 'not forked' do + let(:project) { create(:project) } + + it 'schedules a RepositoryImportWorker job' do + expect(RepositoryImportWorker).to receive(:perform_async).with(project.id) + + project.add_import_job + end + end + end + describe '.where_paths_in' do context 'without any paths' do it 'returns an empty relation' do diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 9262aeb6ed8..1f42fbd3385 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -151,8 +151,8 @@ describe ProjectTeam, models: true do it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) } it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) } it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) } - it { expect(project.team.max_member_access(nonmember.id)).to be_nil } - it { expect(project.team.max_member_access(requester.id)).to be_nil } + it { expect(project.team.max_member_access(nonmember.id)).to eq(Gitlab::Access::NO_ACCESS) } + it { expect(project.team.max_member_access(requester.id)).to eq(Gitlab::Access::NO_ACCESS) } end context 'when project is shared with group' do @@ -168,14 +168,14 @@ describe ProjectTeam, models: true do it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) } it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) } - it { expect(project.team.max_member_access(nonmember.id)).to be_nil } - it { expect(project.team.max_member_access(requester.id)).to be_nil } + it { expect(project.team.max_member_access(nonmember.id)).to eq(Gitlab::Access::NO_ACCESS) } + it { expect(project.team.max_member_access(requester.id)).to eq(Gitlab::Access::NO_ACCESS) } context 'but share_with_group_lock is true' do before { project.namespace.update(share_with_group_lock: true) } - it { expect(project.team.max_member_access(master.id)).to be_nil } - it { expect(project.team.max_member_access(reporter.id)).to be_nil } + it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::NO_ACCESS) } + it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::NO_ACCESS) } end end end @@ -194,8 +194,53 @@ describe ProjectTeam, models: true do it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) } it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) } it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) } - it { expect(project.team.max_member_access(nonmember.id)).to be_nil } - it { expect(project.team.max_member_access(requester.id)).to be_nil } + it { expect(project.team.max_member_access(nonmember.id)).to eq(Gitlab::Access::NO_ACCESS) } + it { expect(project.team.max_member_access(requester.id)).to eq(Gitlab::Access::NO_ACCESS) } + end + end + + describe "#max_member_access_for_users" do + it 'returns correct roles for different users' do + master = create(:user) + reporter = create(:user) + promoted_guest = create(:user) + guest = create(:user) + project = create(:project) + + project.team << [master, :master] + project.team << [reporter, :reporter] + project.team << [promoted_guest, :guest] + project.team << [guest, :guest] + + group = create(:group) + group_developer = create(:user) + second_developer = create(:user) + project.project_group_links.create( + group: group, + group_access: Gitlab::Access::DEVELOPER) + + group.add_master(promoted_guest) + group.add_developer(group_developer) + group.add_developer(second_developer) + + second_group = create(:group) + project.project_group_links.create( + group: second_group, + group_access: Gitlab::Access::MASTER) + second_group.add_master(second_developer) + + users = [master, reporter, promoted_guest, guest, group_developer, second_developer].map(&:id) + + expected = { + master.id => Gitlab::Access::MASTER, + reporter.id => Gitlab::Access::REPORTER, + promoted_guest.id => Gitlab::Access::DEVELOPER, + guest.id => Gitlab::Access::GUEST, + group_developer.id => Gitlab::Access::DEVELOPER, + second_developer.id => Gitlab::Access::MASTER + } + + expect(project.team.max_member_access_for_user_ids(users)).to eq(expected) end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 59c5732c075..5bc1bd9a930 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -446,6 +446,43 @@ describe Repository, models: true do end.to raise_error(GitHooksService::PreReceiveError) end end + + context 'when target branch is different from source branch' do + before do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, '']) + end + + it 'expires branch cache' do + expect(repository).not_to receive(:expire_exists_cache) + expect(repository).not_to receive(:expire_root_ref_cache) + expect(repository).not_to receive(:expire_emptiness_caches) + expect(repository).to receive(:expire_branches_cache) + expect(repository).to receive(:expire_has_visible_content_cache) + expect(repository).to receive(:expire_branch_count_cache) + + repository.commit_with_hooks(user, 'new-feature') { sample_commit.id } + end + end + + context 'when repository is empty' do + before do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, '']) + end + + it 'expires creation and branch cache' do + empty_repository = create(:empty_project, :empty_repo).repository + + expect(empty_repository).to receive(:expire_exists_cache) + expect(empty_repository).to receive(:expire_root_ref_cache) + expect(empty_repository).to receive(:expire_emptiness_caches) + expect(empty_repository).to receive(:expire_branches_cache) + expect(empty_repository).to receive(:expire_has_visible_content_cache) + expect(empty_repository).to receive(:expire_branch_count_cache) + + empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!', + 'Updates file content', 'master', false) + end + end end describe '#exists?' do @@ -1172,35 +1209,31 @@ describe Repository, models: true do end end - describe '.clean_old_archives' do - let(:path) { Gitlab.config.gitlab.repository_downloads_path } - - context 'when the downloads directory does not exist' do - it 'does not remove any archives' do - expect(File).to receive(:directory?).with(path).and_return(false) + describe "#keep_around" do + it "does not fail if we attempt to reference bad commit" do + expect(repository.kept_around?('abc1234')).to be_falsey + end - expect(Gitlab::Popen).not_to receive(:popen) + it "stores a reference to the specified commit sha so it isn't garbage collected" do + repository.keep_around(sample_commit.id) - described_class.clean_old_archives - end + expect(repository.kept_around?(sample_commit.id)).to be_truthy end - context 'when the downloads directory exists' do - it 'removes old archives' do - expect(File).to receive(:directory?).with(path).and_return(true) + it "attempting to call keep_around on truncated ref does not fail" do + repository.keep_around(sample_commit.id) + ref = repository.send(:keep_around_ref_name, sample_commit.id) + path = File.join(repository.path, ref) + # Corrupt the reference + File.truncate(path, 0) - expect(Gitlab::Popen).to receive(:popen) + expect(repository.kept_around?(sample_commit.id)).to be_falsey - described_class.clean_old_archives - end - end - end - - describe "#keep_around" do - it "stores a reference to the specified commit sha so it isn't garbage collected" do repository.keep_around(sample_commit.id) - expect(repository.kept_around?(sample_commit.id)).to be_truthy + expect(repository.kept_around?(sample_commit.id)).to be_falsey + + File.delete(path) end end diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index f5b39c3d698..86a7b242fbe 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' -describe API::API, api: true do +describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } let(:api_user) { user } - let(:user2) { create(:user) } let!(:project) { create(:project, creator_id: user.id) } let!(:developer) { create(:project_member, :developer, user: user, project: project) } - let!(:reporter) { create(:project_member, :reporter, user: user2, project: project) } - let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id) } + let(:reporter) { create(:project_member, :reporter, project: project) } + let(:guest) { create(:project_member, :guest, project: project) } + let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) } let!(:build) { create(:ci_build, pipeline: pipeline) } describe 'GET /projects/:id/builds ' do @@ -172,10 +172,104 @@ describe API::API, api: true do end end + describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do + let(:api_user) { reporter.user } + let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } + + def path_for_ref(ref = pipeline.ref, job = build.name) + api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", api_user) + end + + context 'when not logged in' do + let(:api_user) { nil } + + before do + get path_for_ref + end + + it 'gives 401' do + expect(response).to have_http_status(401) + end + end + + context 'when logging as guest' do + let(:api_user) { guest.user } + + before do + get path_for_ref + end + + it 'gives 403' do + expect(response).to have_http_status(403) + end + end + + context 'non-existing build' do + shared_examples 'not found' do + it { expect(response).to have_http_status(:not_found) } + end + + context 'has no such ref' do + before do + get path_for_ref('TAIL', build.name) + end + + it_behaves_like 'not found' + end + + context 'has no such build' do + before do + get path_for_ref(pipeline.ref, 'NOBUILD') + end + + it_behaves_like 'not found' + end + end + + context 'find proper build' do + shared_examples 'a valid file' do + let(:download_headers) do + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => + "attachment; filename=#{build.artifacts_file.filename}" } + end + + it { expect(response).to have_http_status(200) } + it { expect(response.headers).to include(download_headers) } + end + + context 'with regular branch' do + before do + pipeline.update(ref: 'master', + sha: project.commit('master').sha) + + get path_for_ref('master') + end + + it_behaves_like 'a valid file' + end + + context 'with branch name containing slash' do + before do + pipeline.update(ref: 'improve/awesome', + sha: project.commit('improve/awesome').sha) + end + + before do + get path_for_ref('improve/awesome') + end + + it_behaves_like 'a valid file' + end + end + end + describe 'GET /projects/:id/builds/:build_id/trace' do let(:build) { create(:ci_build, :trace, pipeline: pipeline) } - before { get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) } + before do + get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) + end context 'authorized user' do it 'should return specific build trace' do @@ -205,7 +299,7 @@ describe API::API, api: true do end context 'user without :update_build permission' do - let(:api_user) { user2 } + let(:api_user) { reporter.user } it 'should not cancel build' do expect(response).to have_http_status(403) @@ -237,7 +331,7 @@ describe API::API, api: true do end context 'user without :update_build permission' do - let(:api_user) { user2 } + let(:api_user) { reporter.user } it 'should not retry build' do expect(response).to have_http_status(403) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index afa0599807f..8c6a7e6529d 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -396,7 +396,6 @@ describe API::API, api: true do expect(json_response['alt']).to eq("dk") expect(json_response['url']).to start_with("/uploads/") expect(json_response['url']).to end_with("/dk.png") - expect(json_response['is_image']).to eq(true) end end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 1c7c60ec644..cf1e8d9b514 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -98,7 +98,7 @@ describe Ci::API::API do { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true }, { "key" => "DB_NAME", "value" => "postgres", "public" => true }, { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }, - { "key" => "TRIGGER_KEY", "value" => "TRIGGER_VALUE", "public" => false } + { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false } ) end diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index 8b19936ae6d..69eeb45ed71 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -1,6 +1,5 @@ require 'spec_helper' -# team_update_admin_user PUT /admin/users/:id/team_update(.:format) admin/users#team_update # block_admin_user PUT /admin/users/:id/block(.:format) admin/users#block # unblock_admin_user PUT /admin/users/:id/unblock(.:format) admin/users#unblock # admin_users GET /admin/users(.:format) admin/users#index @@ -11,10 +10,6 @@ require 'spec_helper' # PUT /admin/users/:id(.:format) admin/users#update # DELETE /admin/users/:id(.:format) admin/users#destroy describe Admin::UsersController, "routing" do - it "to #team_update" do - expect(put("/admin/users/1/team_update")).to route_to('admin/users#team_update', id: '1') - end - it "to #block" do expect(put("/admin/users/1/block")).to route_to('admin/users#block', id: '1') end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 620f328a114..9151cd3aefe 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -135,10 +135,6 @@ describe Projects::RepositoriesController, 'routing' do it 'to #archive format:tar.bz2' do expect(get('/gitlab/gitlabhq/repository/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2') end - - it 'to #show' do - expect(get('/gitlab/gitlabhq/repository')).to route_to('projects/repositories#show', namespace_id: 'gitlab', project_id: 'gitlabhq') - end end describe Projects::BranchesController, 'routing' do diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 0a52c1ab933..1d4df9197f6 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -176,18 +176,10 @@ describe Profiles::KeysController, "routing" do expect(post("/profile/keys")).to route_to('profiles/keys#create') end - it "to #edit" do - expect(get("/profile/keys/1/edit")).to route_to('profiles/keys#edit', id: '1') - end - it "to #show" do expect(get("/profile/keys/1")).to route_to('profiles/keys#show', id: '1') end - it "to #update" do - expect(put("/profile/keys/1")).to route_to('profiles/keys#update', id: '1') - end - it "to #destroy" do expect(delete("/profile/keys/1")).to route_to('profiles/keys#destroy', id: '1') end diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb index ba3a4dfc048..321b54ac39d 100644 --- a/spec/services/issues/bulk_update_service_spec.rb +++ b/spec/services/issues/bulk_update_service_spec.rb @@ -1,118 +1,106 @@ require 'spec_helper' describe Issues::BulkUpdateService, services: true do - let(:user) { create(:user) } - let(:project) { Projects::CreateService.new(user, namespace: user.namespace, name: 'test').execute } + let(:user) { create(:user) } + let(:project) { create(:empty_project, namespace: user.namespace) } - let!(:result) { Issues::BulkUpdateService.new(project, user, params).execute } + def bulk_update(issues, extra_params = {}) + bulk_update_params = extra_params + .reverse_merge(issues_ids: Array(issues).map(&:id).join(',')) - describe :close_issue do - let(:issues) { create_list(:issue, 5, project: project) } - let(:params) do - { - state_event: 'close', - issues_ids: issues.map(&:id).join(',') - } - end + Issues::BulkUpdateService.new(project, user, bulk_update_params).execute + end + + describe 'close issues' do + let(:issues) { create_list(:issue, 2, project: project) } it 'succeeds and returns the correct number of issues updated' do + result = bulk_update(issues, state_event: 'close') + expect(result[:success]).to be_truthy expect(result[:count]).to eq(issues.count) end it 'closes all the issues passed' do + bulk_update(issues, state_event: 'close') + expect(project.issues.opened).to be_empty expect(project.issues.closed).not_to be_empty end end - describe :reopen_issues do - let(:issues) { create_list(:closed_issue, 5, project: project) } - let(:params) do - { - state_event: 'reopen', - issues_ids: issues.map(&:id).join(',') - } - end + describe 'reopen issues' do + let(:issues) { create_list(:closed_issue, 2, project: project) } it 'succeeds and returns the correct number of issues updated' do + result = bulk_update(issues, state_event: 'reopen') + expect(result[:success]).to be_truthy expect(result[:count]).to eq(issues.count) end it 'reopens all the issues passed' do + bulk_update(issues, state_event: 'reopen') + expect(project.issues.closed).to be_empty expect(project.issues.opened).not_to be_empty end end describe 'updating assignee' do - let(:issue) do - create(:issue, project: project) { |issue| issue.update_attributes(assignee: user) } - end - - let(:params) do - { - assignee_id: assignee_id, - issues_ids: issue.id.to_s - } - end + let(:issue) { create(:issue, project: project, assignee: user) } context 'when the new assignee ID is a valid user' do - let(:new_assignee) { create(:user) } - let(:assignee_id) { new_assignee.id } - it 'succeeds' do + result = bulk_update(issue, assignee_id: create(:user).id) + expect(result[:success]).to be_truthy expect(result[:count]).to eq(1) end it 'updates the assignee to the use ID passed' do - expect(issue.reload.assignee).to eq(new_assignee) + assignee = create(:user) + + expect { bulk_update(issue, assignee_id: assignee.id) } + .to change { issue.reload.assignee }.from(user).to(assignee) end end context 'when the new assignee ID is -1' do - let(:assignee_id) { -1 } - it 'unassigns the issues' do - expect(issue.reload.assignee).to be_nil + expect { bulk_update(issue, assignee_id: -1) } + .to change { issue.reload.assignee }.to(nil) end end context 'when the new assignee ID is not present' do - let(:assignee_id) { nil } - it 'does not unassign' do - expect(issue.reload.assignee).to eq(user) + expect { bulk_update(issue, assignee_id: nil) } + .not_to change { issue.reload.assignee } end end end describe 'updating milestones' do - let(:issue) { create(:issue, project: project) } + let(:issue) { create(:issue, project: project) } let(:milestone) { create(:milestone, project: project) } - let(:params) do - { - issues_ids: issue.id.to_s, - milestone_id: milestone.id - } - end - it 'succeeds' do + result = bulk_update(issue, milestone_id: milestone.id) + expect(result[:success]).to be_truthy expect(result[:count]).to eq(1) end it 'updates the issue milestone' do - expect(project.issues.first.milestone).to eq(milestone) + expect { bulk_update(issue, milestone_id: milestone.id) } + .to change { issue.reload.milestone }.from(nil).to(milestone) end end describe 'updating labels' do def create_issue_with_labels(labels) - create(:issue, project: project) { |issue| issue.update_attributes(labels: labels) } + create(:labeled_issue, project: project, labels: labels) end let(:bug) { create(:label, project: project) } @@ -129,15 +117,18 @@ describe Issues::BulkUpdateService, services: true do let(:add_labels) { [] } let(:remove_labels) { [] } - let(:params) do + let(:bulk_update_params) do { - label_ids: labels.map(&:id), - add_label_ids: add_labels.map(&:id), + label_ids: labels.map(&:id), + add_label_ids: add_labels.map(&:id), remove_label_ids: remove_labels.map(&:id), - issues_ids: issues.map(&:id).join(',') } end + before do + bulk_update(issues, bulk_update_params) + end + context 'when label_ids are passed' do let(:issues) { [issue_all_labels, issue_no_labels] } let(:labels) { [bug, regression] } @@ -263,40 +254,28 @@ describe Issues::BulkUpdateService, services: true do end end - describe :subscribe_issues do - let(:issues) { create_list(:issue, 5, project: project) } - let(:params) do - { - subscription_event: 'subscribe', - issues_ids: issues.map(&:id).join(',') - } - end + describe 'subscribe to issues' do + let(:issues) { create_list(:issue, 2, project: project) } it 'subscribes the given user' do - issues.each do |issue| - expect(issue.subscribed?(user)).to be_truthy - end - end - end + bulk_update(issues, subscription_event: 'subscribe') - describe :unsubscribe_issues do - let(:issues) { create_list(:closed_issue, 5, project: project) } - let(:params) do - { - subscription_event: 'unsubscribe', - issues_ids: issues.map(&:id).join(',') - } + expect(issues).to all(be_subscribed(user)) end + end - before do - issues.each do |issue| + describe 'unsubscribe from issues' do + let(:issues) do + create_list(:closed_issue, 2, project: project) do |issue| issue.subscriptions.create(user: user, subscribed: true) end end it 'unsubscribes the given user' do + bulk_update(issues, subscription_event: 'unsubscribe') + issues.each do |issue| - expect(issue.subscribed?(user)).to be_falsey + expect(issue).not_to be_subscribed(user) end end end diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb index f252e2c5902..122a7cea2a1 100644 --- a/spec/services/projects/download_service_spec.rb +++ b/spec/services/projects/download_service_spec.rb @@ -35,8 +35,6 @@ describe Projects::DownloadService, services: true do it { expect(@link_to_file).to have_key(:alt) } it { expect(@link_to_file).to have_key(:url) } - it { expect(@link_to_file).to have_key(:is_image) } - it { expect(@link_to_file[:is_image]).to be true } it { expect(@link_to_file[:url]).to match('rails_sample.jpg') } it { expect(@link_to_file[:alt]).to eq('rails_sample') } end @@ -49,8 +47,6 @@ describe Projects::DownloadService, services: true do it { expect(@link_to_file).to have_key(:alt) } it { expect(@link_to_file).to have_key(:url) } - it { expect(@link_to_file).to have_key(:is_image) } - it { expect(@link_to_file[:is_image]).to be false } it { expect(@link_to_file[:url]).to match('doc_sample.txt') } it { expect(@link_to_file[:alt]).to eq('doc_sample.txt') } end diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb index 9268a9fb1a2..c42eeba4b9c 100644 --- a/spec/services/projects/upload_service_spec.rb +++ b/spec/services/projects/upload_service_spec.rb @@ -15,9 +15,7 @@ describe Projects::UploadService, services: true do it { expect(@link_to_file).to have_key(:alt) } it { expect(@link_to_file).to have_key(:url) } - it { expect(@link_to_file).to have_key(:is_image) } it { expect(@link_to_file).to have_value('banana_sample') } - it { expect(@link_to_file[:is_image]).to equal(true) } it { expect(@link_to_file[:url]).to match('banana_sample.gif') } end @@ -31,8 +29,6 @@ describe Projects::UploadService, services: true do it { expect(@link_to_file).to have_key(:alt) } it { expect(@link_to_file).to have_key(:url) } it { expect(@link_to_file).to have_value('dk') } - it { expect(@link_to_file).to have_key(:is_image) } - it { expect(@link_to_file[:is_image]).to equal(true) } it { expect(@link_to_file[:url]).to match('dk.png') } end @@ -44,9 +40,7 @@ describe Projects::UploadService, services: true do it { expect(@link_to_file).to have_key(:alt) } it { expect(@link_to_file).to have_key(:url) } - it { expect(@link_to_file).to have_key(:is_image) } it { expect(@link_to_file).to have_value('rails_sample') } - it { expect(@link_to_file[:is_image]).to equal(true) } it { expect(@link_to_file[:url]).to match('rails_sample.jpg') } end @@ -58,9 +52,7 @@ describe Projects::UploadService, services: true do it { expect(@link_to_file).to have_key(:alt) } it { expect(@link_to_file).to have_key(:url) } - it { expect(@link_to_file).to have_key(:is_image) } it { expect(@link_to_file).to have_value('doc_sample.txt') } - it { expect(@link_to_file[:is_image]).to equal(false) } it { expect(@link_to_file[:url]).to match('doc_sample.txt') } end diff --git a/spec/services/repository_archive_clean_up_service_spec.rb b/spec/services/repository_archive_clean_up_service_spec.rb new file mode 100644 index 00000000000..842585f9e54 --- /dev/null +++ b/spec/services/repository_archive_clean_up_service_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +describe RepositoryArchiveCleanUpService, services: true do + describe '#execute' do + subject(:service) { described_class.new } + + context 'when the downloads directory does not exist' do + it 'does not remove any archives' do + path = '/invalid/path/' + stub_repository_downloads_path(path) + + expect(File).to receive(:directory?).with(path).and_return(false) + expect(service).not_to receive(:clean_up_old_archives) + expect(service).not_to receive(:clean_up_empty_directories) + + service.execute + end + end + + context 'when the downloads directory exists' do + shared_examples 'invalid archive files' do |dirname, extensions, mtime| + it 'does not remove files and directoy' do + in_directory_with_files(dirname, extensions, mtime) do |dir, files| + service.execute + + files.each { |file| expect(File.exist?(file)).to eq true } + expect(File.directory?(dir)).to eq true + end + end + end + + it 'removes files older than 2 hours that matches valid archive extensions' do + in_directory_with_files('sample.git', %w[tar tar.bz2 tar.gz zip], 2.hours) do |dir, files| + service.execute + + files.each { |file| expect(File.exist?(file)).to eq false } + expect(File.directory?(dir)).to eq false + end + end + + context 'with files older than 2 hours that does not matches valid archive extensions' do + it_behaves_like 'invalid archive files', 'sample.git', %w[conf rb], 2.hours + end + + context 'with files older than 2 hours inside invalid directories' do + it_behaves_like 'invalid archive files', 'john_doe/sample.git', %w[conf rb tar tar.gz], 2.hours + end + + context 'with files newer than 2 hours that matches valid archive extensions' do + it_behaves_like 'invalid archive files', 'sample.git', %w[tar tar.bz2 tar.gz zip], 1.hour + end + + context 'with files newer than 2 hours that does not matches valid archive extensions' do + it_behaves_like 'invalid archive files', 'sample.git', %w[conf rb], 1.hour + end + + context 'with files newer than 2 hours inside invalid directories' do + it_behaves_like 'invalid archive files', 'sample.git', %w[conf rb tar tar.gz], 1.hour + end + end + + def in_directory_with_files(dirname, extensions, mtime) + Dir.mktmpdir do |tmpdir| + stub_repository_downloads_path(tmpdir) + dir = File.join(tmpdir, dirname) + files = create_temporary_files(dir, extensions, mtime) + + yield(dir, files) + end + end + + def stub_repository_downloads_path(path) + allow(Gitlab.config.gitlab).to receive(:repository_downloads_path).and_return(path) + end + + def create_temporary_files(dir, extensions, mtime) + FileUtils.mkdir_p(dir) + FileUtils.touch(extensions.map { |ext| File.join(dir, "sample.#{ext}") }, mtime: Time.now - mtime) + end + end +end diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index e005058ba5b..8c98b1f988c 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -178,6 +178,17 @@ module MarkdownMatchers expect(actual).to have_selector('span.idiff.deletion', count: 2) end end + + # VideoLinkFilter + matcher :parse_video_links do + set_default_markdown_messages + + match do |actual| + video = actual.at_css('video') + + expect(video['src']).to end_with('/assets/videos/gitlab-demo.mp4') + end + end end # Monkeypatch the matcher DSL so that we can reduce some noisy duplication for diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 83f2ad96fd8..3735abe2302 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -20,7 +20,8 @@ module TestEnv 'gitattributes' => '5a62481', 'expand-collapse-diffs' => '4842455', 'expand-collapse-files' => '025db92', - 'expand-collapse-lines' => '238e82d' + 'expand-collapse-lines' => '238e82d', + 'video' => '8879059' } # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb new file mode 100644 index 00000000000..e8300abed5d --- /dev/null +++ b/spec/uploaders/file_uploader_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe FileUploader do + let(:project) { create(:project) } + + before do + @previous_enable_processing = FileUploader.enable_processing + FileUploader.enable_processing = false + @uploader = FileUploader.new(project) + end + + after do + FileUploader.enable_processing = @previous_enable_processing + @uploader.remove! + end + + describe '#image_or_video?' do + context 'given an image file' do + before do + @uploader.store!(File.new(Rails.root.join('spec', 'fixtures', 'rails_sample.jpg'))) + end + + it 'detects an image based on file extension' do + expect(@uploader.image_or_video?).to be true + end + end + + context 'given an video file' do + before do + video_file = File.new(Rails.root.join('spec', 'fixtures', 'video_sample.mp4')) + @uploader.store!(video_file) + end + + it 'detects a video based on file extension' do + expect(@uploader.image_or_video?).to be true + end + end + + it 'does not return image_or_video? for other types' do + @uploader.store!(File.new(Rails.root.join('spec', 'fixtures', 'doc_sample.txt'))) + + expect(@uploader.image_or_video?).to be false + end + end +end diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb new file mode 100644 index 00000000000..dae858a52f6 --- /dev/null +++ b/spec/views/admin/dashboard/index.html.haml_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe 'admin/dashboard/index.html.haml' do + include Devise::TestHelpers + + before do + assign(:projects, create_list(:empty_project, 1)) + assign(:users, create_list(:user, 1)) + assign(:groups, create_list(:group, 1)) + + allow(view).to receive(:admin?).and_return(true) + end + + it "shows version of GitLab Workhorse" do + render + + expect(rendered).to have_content 'GitLab Workhorse' + expect(rendered).to have_content Gitlab::Workhorse.version + end +end diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb index 42220a20c75..464051063d8 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/builds/show.html.haml_spec.rb @@ -44,9 +44,29 @@ describe 'projects/builds/show' do it 'shows commit title and not show commit message' do render - + expect(rendered).to have_css('p.build-light-text.append-bottom-0', text: /\A\n#{Regexp.escape(commit_title)}\n\Z/) end end + + describe 'shows trigger variables in sidebar' do + let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline) } + + before do + build.trigger_request = trigger_request + render + end + + it 'shows trigger variables in separate lines' do + expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_1', 'TRIGGER_VALUE_1')) + expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_2', 'TRIGGER_VALUE_2')) + end + end + + private + + def variable_regexp(key, value) + /\A#{Regexp.escape("#{key}=#{value}")}\Z/ + end end diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb index 439da765c2c..796751efe8d 100644 --- a/spec/workers/emails_on_push_worker_spec.rb +++ b/spec/workers/emails_on_push_worker_spec.rb @@ -12,6 +12,42 @@ describe EmailsOnPushWorker do subject { EmailsOnPushWorker.new } describe "#perform" do + context "when push is a new branch" do + let(:email) { ActionMailer::Base.deliveries.last } + + before do + data_new_branch = data.stringify_keys.merge("before" => Gitlab::Git::BLANK_SHA) + + subject.perform(project.id, recipients, data_new_branch) + end + + it "sends a mail with the correct subject" do + expect(email.subject).to include("Pushed new branch") + end + + it "sends the mail to the correct recipient" do + expect(email.to).to eq([user.email]) + end + end + + context "when push is a deleted branch" do + let(:email) { ActionMailer::Base.deliveries.last } + + before do + data_deleted_branch = data.stringify_keys.merge("after" => Gitlab::Git::BLANK_SHA) + + subject.perform(project.id, recipients, data_deleted_branch) + end + + it "sends a mail with the correct subject" do + expect(email.subject).to include("Deleted branch") + end + + it "sends the mail to the correct recipient" do + expect(email.to).to eq([user.email]) + end + end + context "when there are no errors in sending" do let(:email) { ActionMailer::Base.deliveries.last } diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index 5f762282b5e..60605460adb 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -14,21 +14,24 @@ describe RepositoryForkWorker do describe "#perform" do it "creates a new repository from a fork" do expect(shell).to receive(:fork_repository).with( - project.repository_storage_path, + '/test/path', project.path_with_namespace, + project.repository_storage_path, fork_project.namespace.path ).and_return(true) subject.perform( project.id, + '/test/path', project.path_with_namespace, fork_project.namespace.path) end it 'flushes various caches' do expect(shell).to receive(:fork_repository).with( - project.repository_storage_path, + '/test/path', project.path_with_namespace, + project.repository_storage_path, fork_project.namespace.path ).and_return(true) @@ -38,7 +41,7 @@ describe RepositoryForkWorker do expect_any_instance_of(Repository).to receive(:expire_exists_cache). and_call_original - subject.perform(project.id, project.path_with_namespace, + subject.perform(project.id, '/test/path', project.path_with_namespace, fork_project.namespace.path) end @@ -49,6 +52,7 @@ describe RepositoryForkWorker do subject.perform( project.id, + '/test/path', project.path_with_namespace, fork_project.namespace.path) end diff --git a/vendor/assets/javascripts/task_list.js b/vendor/assets/javascripts/task_list.js new file mode 100644 index 00000000000..bc451506b6a --- /dev/null +++ b/vendor/assets/javascripts/task_list.js @@ -0,0 +1,119 @@ + +/*= provides tasklist:enabled */ + + +/*= provides tasklist:disabled */ + + +/*= provides tasklist:change */ + + +/*= provides tasklist:changed */ + +(function() { + var codeFencesPattern, complete, completePattern, disableTaskList, disableTaskLists, enableTaskList, enableTaskLists, escapePattern, incomplete, incompletePattern, itemPattern, itemsInParasPattern, updateTaskList, updateTaskListItem, + indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + + incomplete = "[ ]"; + + complete = "[x]"; + + escapePattern = function(str) { + return str.replace(/([\[\]])/g, "\\$1").replace(/\s/, "\\s").replace("x", "[xX]"); + }; + + incompletePattern = RegExp("" + (escapePattern(incomplete))); + + completePattern = RegExp("" + (escapePattern(complete))); + + itemPattern = RegExp("^(?:\\s*(?:>\\s*)*(?:[-+*]|(?:\\d+\\.)))\\s*(" + (escapePattern(complete)) + "|" + (escapePattern(incomplete)) + ")\\s+(?!\\(.*?\\))(?=(?:\\[.*?\\]\\s*(?:\\[.*?\\]|\\(.*?\\))\\s*)*(?:[^\\[]|$))"); + + codeFencesPattern = /^`{3}(?:\s*\w+)?[\S\s].*[\S\s]^`{3}$/mg; + + itemsInParasPattern = RegExp("^(" + (escapePattern(complete)) + "|" + (escapePattern(incomplete)) + ").+$", "g"); + + updateTaskListItem = function(source, itemIndex, checked) { + var clean, index, line, result; + clean = source.replace(/\r/g, '').replace(codeFencesPattern, '').replace(itemsInParasPattern, '').split("\n"); + index = 0; + result = (function() { + var i, len, ref, results; + ref = source.split("\n"); + results = []; + for (i = 0, len = ref.length; i < len; i++) { + line = ref[i]; + if (indexOf.call(clean, line) >= 0 && line.match(itemPattern)) { + index += 1; + if (index === itemIndex) { + line = checked ? line.replace(incompletePattern, complete) : line.replace(completePattern, incomplete); + } + } + results.push(line); + } + return results; + })(); + return result.join("\n"); + }; + + updateTaskList = function($item) { + var $container, $field, checked, event, index; + $container = $item.closest('.js-task-list-container'); + $field = $container.find('.js-task-list-field'); + index = 1 + $container.find('.task-list-item-checkbox').index($item); + checked = $item.prop('checked'); + event = $.Event('tasklist:change'); + $field.trigger(event, [index, checked]); + if (!event.isDefaultPrevented()) { + $field.val(updateTaskListItem($field.val(), index, checked)); + $field.trigger('change'); + return $field.trigger('tasklist:changed', [index, checked]); + } + }; + + $(document).on('change', '.task-list-item-checkbox', function() { + return updateTaskList($(this)); + }); + + enableTaskList = function($container) { + if ($container.find('.js-task-list-field').length > 0) { + $container.find('.task-list-item').addClass('enabled').find('.task-list-item-checkbox').attr('disabled', null); + return $container.addClass('is-task-list-enabled').trigger('tasklist:enabled'); + } + }; + + enableTaskLists = function($containers) { + var container, i, len, results; + results = []; + for (i = 0, len = $containers.length; i < len; i++) { + container = $containers[i]; + results.push(enableTaskList($(container))); + } + return results; + }; + + disableTaskList = function($container) { + $container.find('.task-list-item').removeClass('enabled').find('.task-list-item-checkbox').attr('disabled', 'disabled'); + return $container.removeClass('is-task-list-enabled').trigger('tasklist:disabled'); + }; + + disableTaskLists = function($containers) { + var container, i, len, results; + results = []; + for (i = 0, len = $containers.length; i < len; i++) { + container = $containers[i]; + results.push(disableTaskList($(container))); + } + return results; + }; + + $.fn.taskList = function(method) { + var $container, methods; + $container = $(this).closest('.js-task-list-container'); + methods = { + enable: enableTaskLists, + disable: disableTaskLists + }; + return methods[method || 'enable']($container); + }; + +}).call(this); diff --git a/vendor/assets/javascripts/task_list.js.coffee b/vendor/assets/javascripts/task_list.js.coffee deleted file mode 100644 index 584751af8ea..00000000000 --- a/vendor/assets/javascripts/task_list.js.coffee +++ /dev/null @@ -1,258 +0,0 @@ -# The MIT License (MIT) -# -# Copyright (c) 2014 GitHub, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -# TaskList Behavior -# -#= provides tasklist:enabled -#= provides tasklist:disabled -#= provides tasklist:change -#= provides tasklist:changed -# -# -# Enables Task List update behavior. -# -# ### Example Markup -# -# <div class="js-task-list-container"> -# <ul class="task-list"> -# <li class="task-list-item"> -# <input type="checkbox" class="js-task-list-item-checkbox" disabled /> -# text -# </li> -# </ul> -# <form> -# <textarea class="js-task-list-field">- [ ] text</textarea> -# </form> -# </div> -# -# ### Specification -# -# TaskLists MUST be contained in a `(div).js-task-list-container`. -# -# TaskList Items SHOULD be an a list (`UL`/`OL`) element. -# -# Task list items MUST match `(input).task-list-item-checkbox` and MUST be -# `disabled` by default. -# -# TaskLists MUST have a `(textarea).js-task-list-field` form element whose -# `value` attribute is the source (Markdown) to be udpated. The source MUST -# follow the syntax guidelines. -# -# TaskList updates trigger `tasklist:change` events. If the change is -# successful, `tasklist:changed` is fired. The change can be canceled. -# -# jQuery is required. -# -# ### Methods -# -# `.taskList('enable')` or `.taskList()` -# -# Enables TaskList updates for the container. -# -# `.taskList('disable')` -# -# Disables TaskList updates for the container. -# -## ### Events -# -# `tasklist:enabled` -# -# Fired when the TaskList is enabled. -# -# * **Synchronicity** Sync -# * **Bubbles** Yes -# * **Cancelable** No -# * **Target** `.js-task-list-container` -# -# `tasklist:disabled` -# -# Fired when the TaskList is disabled. -# -# * **Synchronicity** Sync -# * **Bubbles** Yes -# * **Cancelable** No -# * **Target** `.js-task-list-container` -# -# `tasklist:change` -# -# Fired before the TaskList item change takes affect. -# -# * **Synchronicity** Sync -# * **Bubbles** Yes -# * **Cancelable** Yes -# * **Target** `.js-task-list-field` -# -# `tasklist:changed` -# -# Fired once the TaskList item change has taken affect. -# -# * **Synchronicity** Sync -# * **Bubbles** Yes -# * **Cancelable** No -# * **Target** `.js-task-list-field` -# -# ### NOTE -# -# Task list checkboxes are rendered as disabled by default because rendered -# user content is cached without regard for the viewer. - -incomplete = "[ ]" -complete = "[x]" - -# Escapes the String for regular expression matching. -escapePattern = (str) -> - str. - replace(/([\[\]])/g, "\\$1"). # escape square brackets - replace(/\s/, "\\s"). # match all white space - replace("x", "[xX]") # match all cases - -incompletePattern = /// - #{escapePattern(incomplete)} -/// -completePattern = /// - #{escapePattern(complete)} -/// - -# Pattern used to identify all task list items. -# Useful when you need iterate over all items. -itemPattern = /// - ^ - (?: # prefix, consisting of - \s* # optional leading whitespace - (?:>\s*)* # zero or more blockquotes - (?:[-+*]|(?:\d+\.)) # list item indicator - ) - \s* # optional whitespace prefix - ( # checkbox - #{escapePattern(complete)}| - #{escapePattern(incomplete)} - ) - \s+ # is followed by whitespace - (?! - \(.*?\) # is not part of a [foo](url) link - ) - (?= # and is followed by zero or more links - (?:\[.*?\]\s*(?:\[.*?\]|\(.*?\))\s*)* - (?:[^\[]|$) # and either a non-link or the end of the string - ) -/// - -# Used to filter out code fences from the source for comparison only. -# http://rubular.com/r/x5EwZVrloI -# Modified slightly due to issues with JS -codeFencesPattern = /// - ^`{3} # ``` - (?:\s*\w+)? # followed by optional language - [\S\s] # whitespace - .* # code - [\S\s] # whitespace - ^`{3}$ # ``` -///mg - -# Used to filter out potential mismatches (items not in lists). -# http://rubular.com/r/OInl6CiePy -itemsInParasPattern = /// - ^ - ( - #{escapePattern(complete)}| - #{escapePattern(incomplete)} - ) - .+ - $ -///g - -# Given the source text, updates the appropriate task list item to match the -# given checked value. -# -# Returns the updated String text. -updateTaskListItem = (source, itemIndex, checked) -> - clean = source.replace(/\r/g, '').replace(codeFencesPattern, ''). - replace(itemsInParasPattern, '').split("\n") - index = 0 - result = for line in source.split("\n") - if line in clean && line.match(itemPattern) - index += 1 - if index == itemIndex - line = - if checked - line.replace(incompletePattern, complete) - else - line.replace(completePattern, incomplete) - line - result.join("\n") - -# Updates the $field value to reflect the state of $item. -# Triggers the `tasklist:change` event before the value has changed, and fires -# a `tasklist:changed` event once the value has changed. -updateTaskList = ($item) -> - $container = $item.closest '.js-task-list-container' - $field = $container.find '.js-task-list-field' - index = 1 + $container.find('.task-list-item-checkbox').index($item) - checked = $item.prop 'checked' - - event = $.Event 'tasklist:change' - $field.trigger event, [index, checked] - - unless event.isDefaultPrevented() - $field.val updateTaskListItem($field.val(), index, checked) - $field.trigger 'change' - $field.trigger 'tasklist:changed', [index, checked] - -# When the task list item checkbox is updated, submit the change -$(document).on 'change', '.task-list-item-checkbox', -> - updateTaskList $(this) - -# Enables TaskList item changes. -enableTaskList = ($container) -> - if $container.find('.js-task-list-field').length > 0 - $container. - find('.task-list-item').addClass('enabled'). - find('.task-list-item-checkbox').attr('disabled', null) - $container.addClass('is-task-list-enabled'). - trigger 'tasklist:enabled' - -# Enables a collection of TaskList containers. -enableTaskLists = ($containers) -> - for container in $containers - enableTaskList $(container) - -# Disable TaskList item changes. -disableTaskList = ($container) -> - $container. - find('.task-list-item').removeClass('enabled'). - find('.task-list-item-checkbox').attr('disabled', 'disabled') - $container.removeClass('is-task-list-enabled'). - trigger 'tasklist:disabled' - -# Disables a collection of TaskList containers. -disableTaskLists = ($containers) -> - for container in $containers - disableTaskList $(container) - -$.fn.taskList = (method) -> - $container = $(this).closest('.js-task-list-container') - - methods = - enable: enableTaskLists - disable: disableTaskLists - - methods[method || 'enable']($container) diff --git a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml index 0b329aaf1c4..00f9541e89b 100644 --- a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml @@ -2,7 +2,7 @@ # The image already has Hex installed. You might want to consider to use `elixir:latest` image: trenpixster/elixir:latest -# Pic zero or more services to be used on all builds. +# Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. # Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service services: |