Commit 94611607 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into pipeline-emails

* upstream/master: (292 commits)
  Deletes extra empty line breaking the build
  Optimize the `award_user_list` helper spec
  Fix typo and add he MWBS accronym for "Merge When Build Succeeds"
  Added missing content and improved layout
  ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
  Improve the contribution and MR review guide
  Updates test in order to look for link
  Make projects API docs match parameter style
  Fix Event#reset_project_activity updates
  Update user whitelist reject message
  Call ensure_secret_token! in secret token test's before block since it would be called in an initializer.
  Add a CHANGELOG for CacheMarkdownField
  Enable CacheMarkdownField for the remaining models
  Make search results use the markdown cache columns, treating them consistently
  Use CacheMarkdownField for notes
  Add markdown cache columns to the database, but don't use them yet
  Update issue board spec
  Link to Registry docs from project settings
  Truncate long labels with ellipsis in labels page
  Improve issue load time performance by avoiding ORDER BY in find_by call
  ...
parents 720968cc 28ca8502
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
"always-semicolon": true, "always-semicolon": true,
"color-case": "lower", "color-case": "lower",
"block-indent": " ", "block-indent": " ",
"color-shorthand": true, "color-shorthand": false,
"element-case": "lower", "element-case": "lower",
"space-before-colon": "", "space-before-colon": "",
"space-after-colon": " ", "space-after-colon": " ",
......
...@@ -207,9 +207,7 @@ rubocop: *exec ...@@ -207,9 +207,7 @@ rubocop: *exec
rake haml_lint: *exec rake haml_lint: *exec
rake scss_lint: *exec rake scss_lint: *exec
rake brakeman: *exec rake brakeman: *exec
rake flay: rake flay: *exec
<<: *exec
allow_failure: yes
license_finder: *exec license_finder: *exec
rake downtime_check: *exec rake downtime_check: *exec
......
...@@ -453,6 +453,10 @@ Style/VariableName: ...@@ -453,6 +453,10 @@ Style/VariableName:
EnforcedStyle: snake_case EnforcedStyle: snake_case
Enabled: true Enabled: true
# Use the configured style when numbering variables.
Style/VariableNumber:
Enabled: false
# Use when x then ... for one-line cases. # Use when x then ... for one-line cases.
Style/WhenThen: Style/WhenThen:
Enabled: true Enabled: true
......
This diff is collapsed.
...@@ -79,7 +79,7 @@ linters: ...@@ -79,7 +79,7 @@ linters:
# HEX colors should use three-character values where possible. # HEX colors should use three-character values where possible.
HexLength: HexLength:
enabled: true enabled: false
# HEX color values should use lower-case colors to differentiate between # HEX color values should use lower-case colors to differentiate between
# letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`. # letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.13.0 (unreleased) v 8.13.0 (unreleased)
- Truncate long labels with ellipsis in labels page
- Update runner version only when updating contacted_at
- Add link from system note to compare with previous version - Add link from system note to compare with previous version
- Improve issue load time performance by avoiding ORDER BY in find_by call
- Use gitlab-shell v3.6.2 (GIT TRACE logging) - Use gitlab-shell v3.6.2 (GIT TRACE logging)
- Add `/projects/visible` API endpoint (Ben Boeckel)
- Fix centering of custom header logos (Ashley Dumaine) - Fix centering of custom header logos (Ashley Dumaine)
- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
- AbstractReferenceFilter caches project_refs on RequestStore when active - AbstractReferenceFilter caches project_refs on RequestStore when active
- Replaced the check sign to arrow in the show build view. !6501 - Replaced the check sign to arrow in the show build view. !6501
- Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
- Speed-up group milestones show page - Speed-up group milestones show page
- Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
- Add tag shortcut from the Commit page. !6543
- Keep refs for each deployment
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller) - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
- Add more tests for calendar contribution (ClemMakesApps) - Add more tests for calendar contribution (ClemMakesApps)
- Cache rendered markdown in the database, rather than Redis
- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
- Simplify Mentionable concern instance methods
- Fix permission for setting an issue's due date - Fix permission for setting an issue's due date
- API: Multi-file commit !6096 (mahcsig)
- Revert "Label list shows all issues (opened or closed) with that label"
- Expose expires_at field when sharing project on API - Expose expires_at field when sharing project on API
- Fix VueJS template tags being rendered in code comments
- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell) - Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
- Add Issue Board API support (andrebsguedes)
- Allow the Koding integration to be configured through the API - Allow the Koding integration to be configured through the API
- Add new issue button to each list on Issues Board
- Added soft wrap button to repository file/blob editor - Added soft wrap button to repository file/blob editor
- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps) - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
- Fix todos page mobile viewport layout (ClemMakesApps)
- Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps)
- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison) - Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska) - Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
- Fix that manual jobs would no longer block jobs in the next stage. !6604
- Add configurable email subject suffix (Fu Xu) - Add configurable email subject suffix (Fu Xu)
- Added tooltip to fork count on project show page. (Justin DiPierro)
- Use a ConnectionPool for Rails.cache on Sidekiq servers - Use a ConnectionPool for Rails.cache on Sidekiq servers
- Replace `alias_method_chain` with `Module#prepend` - Replace `alias_method_chain` with `Module#prepend`
- Enable GitLab Import/Export for non-admin users. - Enable GitLab Import/Export for non-admin users.
- Preserve label filters when sorting !6136 (Joseph Frazier) - Preserve label filters when sorting !6136 (Joseph Frazier)
- MergeRequest#new form load diff asynchronously
- Only update issuable labels if they have been changed - Only update issuable labels if they have been changed
- Take filters in account in issuable counters. !6496 - Take filters in account in issuable counters. !6496
- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*) - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
- Prevent flash alert text from being obscured when container is fluid
- Append issue template to existing description !6149 (Joseph Frazier)
- Trending projects now only show public projects and the list of projects is cached for a day
- Memoize Gitlab Shell's secret token (!6599, Justin DiPierro)
- Revoke button in Applications Settings underlines on hover. - Revoke button in Applications Settings underlines on hover.
- Use higher size on Gitlab::Redis connection pool on Sidekiq servers
- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska) - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
- Fix Long commit messages overflow viewport in file tree - Fix Long commit messages overflow viewport in file tree
- Revert avoid touching file system on Build#artifacts? - Revert avoid touching file system on Build#artifacts?
- Stop using a Redis lease when updating the project activity timestamp whenever a new event is created
- Add broadcast messages and alerts below sub-nav - Add broadcast messages and alerts below sub-nav
- Better empty state for Groups view - Better empty state for Groups view
- Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe) - Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
- Replace bootstrap caret with fontawesome caret (ClemMakesApps)
- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533 - Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
- Add organization field to user profile - Add organization field to user profile
- Fix deploy status responsiveness error !6633
- Fix resolved discussion display in side-by-side diff view !6575 - Fix resolved discussion display in side-by-side diff view !6575
- Optimize GitHub importing for speed and memory - Optimize GitHub importing for speed and memory
- API: expose pipeline data in builds API (!6502, Guilherme Salazar) - API: expose pipeline data in builds API (!6502, Guilherme Salazar)
- Notify the Merger about merge after successful build (Dimitris Karakasilis) - Notify the Merger about merge after successful build (Dimitris Karakasilis)
- Reduce queries needed to find users using their SSH keys when pushing commits
- Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska)
- Fix broken repository 500 errors in project list - Fix broken repository 500 errors in project list
- Fix Pipeline list commit column width should be adjusted
- Close todos when accepting merge requests via the API !6486 (tonygambone) - Close todos when accepting merge requests via the API !6486 (tonygambone)
- Changed Slack service user referencing from full name to username (Sebastian Poxhofer) - Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
- Retouch environments list and deployments list
v 8.12.4 (unreleased) - Add Container Registry on/off status to Admin Area !6638 (the-undefined)
- Grouped pipeline dropdown is a scrollable container
v 8.12.5 (unreleased)
v 8.12.4
- Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. !6294 (lukehowell)
- Fix padding in build sidebar. !6506
- Changed compare dropdowns to dropdowns with isolated search input. !6550
- Fix race condition on LFS Token. !6592
- Fix type mismatch bug when closing Jira issue. !6619
- Fix lint-doc error. !6623
- Skip wiki creation when GitHub project has wiki enabled. !6665
- Fix issues importing services via Import/Export. !6667
- Restrict failed login attempts for users with 2FA enabled. !6668
- Fix failed project deletion when feature visibility set to private. !6688
- Prevent claiming associated model IDs via import.
- Set GitLab project exported file permissions to owner only
- Change user & group landing page routing from /u/:username to /:username
v 8.12.3 v 8.12.3
- Update Gitlab Shell to support low IO priority for storage moves - Update Gitlab Shell to support low IO priority for storage moves
v 8.12.2 (unreleased) v 8.12.2
- Fix Import/Export not recognising correctly the imported services. - Fix Import/Export not recognising correctly the imported services.
- Fix snippets pagination - Fix snippets pagination
- Fix "Create project" button layout when visibility options are restricted - Fix "Create project" button layout when visibility options are restricted
...@@ -63,6 +112,7 @@ v 8.12.2 (unreleased) ...@@ -63,6 +112,7 @@ v 8.12.2 (unreleased)
- Only update issuable labels if they have been changed - Only update issuable labels if they have been changed
- Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv) - Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv)
- Fix resolve discussion buttons endpoint path - Fix resolve discussion buttons endpoint path
- Refactor remnants of CoffeeScript destructured opts and super !6261
v 8.12.1 v 8.12.1
- Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST - Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST
...@@ -103,6 +153,7 @@ v 8.12.0 ...@@ -103,6 +153,7 @@ v 8.12.0
- Fix long comments in diffs messing with table width - Fix long comments in diffs messing with table width
- Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman) - Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman)
- Fix pagination on user snippets page - Fix pagination on user snippets page
- Honor "fixed layout" preference in more places !6422
- Run CI builds with the permissions of users !5735 - Run CI builds with the permissions of users !5735
- Fix sorting of issues in API - Fix sorting of issues in API
- Fix download artifacts button links !6407 - Fix download artifacts button links !6407
...@@ -119,6 +170,7 @@ v 8.12.0 ...@@ -119,6 +170,7 @@ v 8.12.0
- Reduce contributions calendar data payload (ClemMakesApps) - Reduce contributions calendar data payload (ClemMakesApps)
- Show all pipelines for merge requests even from discarded commits !6414 - Show all pipelines for merge requests even from discarded commits !6414
- Replace contributions calendar timezone payload with dates (ClemMakesApps) - Replace contributions calendar timezone payload with dates (ClemMakesApps)
- Changed MR widget build status to pipeline status !6335
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel) - Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
- Enable pipeline events by default !6278 - Enable pipeline events by default !6278
- Add pipeline email service !6019 - Add pipeline email service !6019
...@@ -143,6 +195,7 @@ v 8.12.0 ...@@ -143,6 +195,7 @@ v 8.12.0
- Increase ci_builds artifacts_size column to 8-byte integer to allow larger files - Increase ci_builds artifacts_size column to 8-byte integer to allow larger files
- Add textarea autoresize after comment (ClemMakesApps) - Add textarea autoresize after comment (ClemMakesApps)
- Do not write SSH public key 'comments' to authorized_keys !6381 - Do not write SSH public key 'comments' to authorized_keys !6381
- Add due date to issue todos
- Refresh todos count cache when an Issue/MR is deleted - Refresh todos count cache when an Issue/MR is deleted
- Fix branches page dropdown sort alignment (ClemMakesApps) - Fix branches page dropdown sort alignment (ClemMakesApps)
- Hides merge request button on branches page is user doesn't have permissions - Hides merge request button on branches page is user doesn't have permissions
......
...@@ -226,8 +226,7 @@ a feedback issue (if there isn't one already) and leave a comment asking for it ...@@ -226,8 +226,7 @@ a feedback issue (if there isn't one already) and leave a comment asking for it
to be marked as `Accepting merge requests`. Please include screenshots or to be marked as `Accepting merge requests`. Please include screenshots or
wireframes if the feature will also change the UI. wireframes if the feature will also change the UI.
Merge requests can be filed either at [GitLab.com][gitlab-mr-tracker] or at Merge requests should be opened at [GitLab.com][gitlab-mr-tracker].
[github.com][github-mr-tracker].
If you are new to GitLab development (or web development in general), see the If you are new to GitLab development (or web development in general), see the
[I want to contribute!](#i-want-to-contribute) section to get you started with [I want to contribute!](#i-want-to-contribute) section to get you started with
...@@ -246,10 +245,17 @@ tests are least likely to receive timely feedback. The workflow to make a merge ...@@ -246,10 +245,17 @@ tests are least likely to receive timely feedback. The workflow to make a merge
request is as follows: request is as follows:
1. Fork the project into your personal space on GitLab.com 1. Fork the project into your personal space on GitLab.com
1. Create a feature branch, branch away from `master`. 1. Create a feature branch, branch away from `master`
1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code 1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code
1. Add your changes to the [CHANGELOG](CHANGELOG) 1. Add your changes to the [CHANGELOG](CHANGELOG):
1. If you are writing documentation, make sure to read the [documentation styleguide][doc-styleguide] 1. If you are fixing a ~regression issue, you can add your entry to the next
patch release (e.g. `8.12.5` if current version is `8.12.4`)
1. Otherwise, add your entry to the next minor release (e.g. `8.13.0` if
current version is `8.12.4`
1. Please add your entry at a random place among the entries of the targeted
release
1. If you are writing documentation, make sure to follow the
[documentation styleguide][doc-styleguide]
1. If you have multiple commits please combine them into one commit by 1. If you have multiple commits please combine them into one commit by
[squashing them][git-squash] [squashing them][git-squash]
1. Push the commit(s) to your fork 1. Push the commit(s) to your fork
...@@ -258,7 +264,7 @@ request is as follows: ...@@ -258,7 +264,7 @@ request is as follows:
1. The MR description should give a motive for your change and the method you 1. The MR description should give a motive for your change and the method you
used to achieve it, see the [merge request description format] used to achieve it, see the [merge request description format]
(#merge-request-description-format) (#merge-request-description-format)
1. If the MR changes the UI it should include before and after screenshots 1. If the MR changes the UI it should include *Before* and *After* screenshots
1. If the MR changes CSS classes please include the list of affected pages, 1. If the MR changes CSS classes please include the list of affected pages,
`grep css-class ./app -R` `grep css-class ./app -R`
1. Link any relevant [issues][ce-tracker] in the merge request description and 1. Link any relevant [issues][ce-tracker] in the merge request description and
...@@ -270,7 +276,9 @@ request is as follows: ...@@ -270,7 +276,9 @@ request is as follows:
[shell command guidelines](doc/development/shell_commands.md) [shell command guidelines](doc/development/shell_commands.md)
1. If your code creates new files on disk please read the 1. If your code creates new files on disk please read the
[shared files guidelines](doc/development/shared_files.md). [shared files guidelines](doc/development/shared_files.md).
1. When writing commit messages please follow [these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) [guidelines](http://chris.beams.io/posts/git-commit/). 1. When writing commit messages please follow
[these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
[guidelines](http://chris.beams.io/posts/git-commit/).
1. If your merge request adds one or more migrations, make sure to execute all 1. If your merge request adds one or more migrations, make sure to execute all
migrations on a fresh database before the MR is reviewed. If the review leads migrations on a fresh database before the MR is reviewed. If the review leads
to large changes in the MR, do this again once the review is complete. to large changes in the MR, do this again once the review is complete.
...@@ -305,23 +313,6 @@ Please ensure that your merge request meets the contribution acceptance criteria ...@@ -305,23 +313,6 @@ Please ensure that your merge request meets the contribution acceptance criteria
When having your code reviewed and when reviewing merge requests please take the When having your code reviewed and when reviewing merge requests please take the
[code review guidelines](doc/development/code_review.md) into account. [code review guidelines](doc/development/code_review.md) into account.
### Merge request description format
Please submit merge requests using the following template in the merge request
description area. Copy-paste it to retain the markdown format.
```
## What does this MR do?
## Are there points in the code the reviewer needs to double check?
## Why was this MR needed?
## What are the relevant issue numbers?
## Screenshots (if relevant)
```
### Contribution acceptance criteria ### Contribution acceptance criteria
1. The change is as small as possible 1. The change is as small as possible
...@@ -333,8 +324,8 @@ description area. Copy-paste it to retain the markdown format. ...@@ -333,8 +324,8 @@ description area. Copy-paste it to retain the markdown format.
aforementioned failing test aforementioned failing test
1. Your MR initially contains a single commit (please use `git rebase -i` to 1. Your MR initially contains a single commit (please use `git rebase -i` to
squash commits) squash commits)
1. Your changes can merge without problems (if not please merge `master`, never 1. Your changes can merge without problems (if not please rebase if you're the
rebase commits pushed to the remote server) only one working on your feature branch, otherwise, merge `master`)
1. Does not break any existing functionality 1. Does not break any existing functionality
1. Fixes one specific issue or implements one specific feature (do not combine 1. Fixes one specific issue or implements one specific feature (do not combine
things, send separate merge requests if needed) things, send separate merge requests if needed)
...@@ -352,7 +343,10 @@ description area. Copy-paste it to retain the markdown format. ...@@ -352,7 +343,10 @@ description area. Copy-paste it to retain the markdown format.
entire line to follow it. This prevents linting tools from generating warnings. entire line to follow it. This prevents linting tools from generating warnings.
- Don't touch neighbouring lines. As an exception, automatic mass - Don't touch neighbouring lines. As an exception, automatic mass
refactoring modifications may leave style non-compliant. refactoring modifications may leave style non-compliant.
1. If the merge request adds any new libraries (gems, JavaScript libraries, etc.), they should conform to our [Licensing guidelines][license-finder-doc]. See the instructions in that document for help if your MR fails the "license-finder" test with a "Dependencies that need approval" error. 1. If the merge request adds any new libraries (gems, JavaScript libraries,
etc.), they should conform to our [Licensing guidelines][license-finder-doc].
See the instructions in that document for help if your MR fails the
"license-finder" test with a "Dependencies that need approval" error.
## Changes for Stable Releases ## Changes for Stable Releases
...@@ -468,7 +462,6 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor ...@@ -468,7 +462,6 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[accepting-mrs-ce]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests [accepting-mrs-ce]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests
[accepting-mrs-ee]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Accepting+Merge+Requests [accepting-mrs-ee]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Accepting+Merge+Requests
[gitlab-mr-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests [gitlab-mr-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests
[github-mr-tracker]: https://github.com/gitlabhq/gitlabhq/pulls
[gdk]: https://gitlab.com/gitlab-org/gitlab-development-kit [gdk]: https://gitlab.com/gitlab-org/gitlab-development-kit
[git-squash]: https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits [git-squash]: https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits
[closed-merge-requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed [closed-merge-requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed
......
...@@ -110,6 +110,7 @@ gem 'creole', '~> 0.5.0' ...@@ -110,6 +110,7 @@ gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1' gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2' gem 'asciidoctor', '~> 1.5.2'
gem 'rouge', '~> 2.0' gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
...@@ -130,7 +131,7 @@ gem 'state_machines-activerecord', '~> 0.4.0' ...@@ -130,7 +131,7 @@ gem 'state_machines-activerecord', '~> 0.4.0'
gem 'after_commit_queue', '~> 1.3.0' gem 'after_commit_queue', '~> 1.3.0'
# Issue tags # Issue tags
gem 'acts-as-taggable-on', '~> 3.4' gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs # Background jobs
gem 'sidekiq', '~> 4.2' gem 'sidekiq', '~> 4.2'
...@@ -231,7 +232,7 @@ gem 'net-ssh', '~> 3.0.1' ...@@ -231,7 +232,7 @@ gem 'net-ssh', '~> 3.0.1'
gem 'base32', '~> 0.3.0' gem 'base32', '~> 0.3.0'
# Sentry integration # Sentry integration
gem 'sentry-raven', '~> 1.1.0' gem 'sentry-raven', '~> 2.0.0'
gem 'premailer-rails', '~> 1.9.0' gem 'premailer-rails', '~> 1.9.0'
...@@ -295,7 +296,7 @@ group :development, :test do ...@@ -295,7 +296,7 @@ group :development, :test do
gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-spinach', '~> 1.1.0'
gem 'spring-commands-teaspoon', '~> 0.0.2' gem 'spring-commands-teaspoon', '~> 0.0.2'
gem 'rubocop', '~> 0.42.0', require: false gem 'rubocop', '~> 0.43.0', require: false
gem 'rubocop-rspec', '~> 1.5.0', require: false gem 'rubocop-rspec', '~> 1.5.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false gem 'scss_lint', '~> 0.47.0', require: false
gem 'haml_lint', '~> 0.18.2', require: false gem 'haml_lint', '~> 0.18.2', require: false
......
...@@ -44,8 +44,8 @@ GEM ...@@ -44,8 +44,8 @@ GEM
minitest (~> 5.1) minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4) thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1) tzinfo (~> 1.1)
acts-as-taggable-on (3.5.0) acts-as-taggable-on (4.0.0)
activerecord (>= 3.2, < 5) activerecord (>= 4.0)
addressable (2.3.8) addressable (2.3.8)
after_commit_queue (1.3.0) after_commit_queue (1.3.0)
activerecord (>= 3.0) activerecord (>= 3.0)
...@@ -487,7 +487,7 @@ GEM ...@@ -487,7 +487,7 @@ GEM
orm_adapter (0.5.0) orm_adapter (0.5.0)
paranoia (2.1.4) paranoia (2.1.4)
activerecord (~> 4.0) activerecord (~> 4.0)
parser (2.3.1.2) parser (2.3.1.4)
ast (~> 2.2) ast (~> 2.2)
pg (0.18.4) pg (0.18.4)
pkg-config (1.1.7) pkg-config (1.1.7)
...@@ -620,7 +620,7 @@ GEM ...@@ -620,7 +620,7 @@ GEM
rspec-retry (0.4.5) rspec-retry (0.4.5)
rspec-core rspec-core
rspec-support (3.5.0) rspec-support (3.5.0)
rubocop (0.42.0) rubocop (0.43.0)
parser (>= 2.3.1.1, < 3.0) parser (>= 2.3.1.1, < 3.0)
powerpack (~> 0.1) powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0) rainbow (>= 1.99.1, < 3.0)
...@@ -665,8 +665,8 @@ GEM ...@@ -665,8 +665,8 @@ GEM
activesupport (>= 3.1) activesupport (>= 3.1)
select2-rails (3.5.9.3) select2-rails (3.5.9.3)
thor (~> 0.14) thor (~> 0.14)
sentry-raven (1.1.0) sentry-raven (2.0.2)
faraday (>= 0.7.6) faraday (>= 0.7.6, < 0.10.x)
settingslogic (2.0.9) settingslogic (2.0.9)
sexp_processor (4.7.0) sexp_processor (4.7.0)
sham_rack (1.3.6) sham_rack (1.3.6)
...@@ -745,6 +745,9 @@ GEM ...@@ -745,6 +745,9 @@ GEM
tilt (2.0.5) tilt (2.0.5)
timecop (0.8.1) timecop (0.8.1)
timfel-krb5-auth (0.8.3) timfel-krb5-auth (0.8.3)
truncato (0.7.8)
htmlentities (~> 4.3.1)
nokogiri (~> 1.6.1)
turbolinks (2.5.3) turbolinks (2.5.3)
coffee-rails coffee-rails
tzinfo (1.2.2) tzinfo (1.2.2)
...@@ -802,7 +805,7 @@ DEPENDENCIES ...@@ -802,7 +805,7 @@ DEPENDENCIES
RedCloth (~> 4.3.2) RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0) ace-rails-ap (~> 4.1.0)
activerecord-session_store (~> 1.0.0) activerecord-session_store (~> 1.0.0)
acts-as-taggable-on (~> 3.4) acts-as-taggable-on (~> 4.0)
addressable (~> 2.3.8) addressable (~> 2.3.8)
after_commit_queue (~> 1.3.0) after_commit_queue (~> 1.3.0)
akismet (~> 2.0) akismet (~> 2.0)
...@@ -938,7 +941,7 @@ DEPENDENCIES ...@@ -938,7 +941,7 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0) rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5) rspec-retry (~> 0.4.5)
rubocop (~> 0.42.0) rubocop (~> 0.43.0)
rubocop-rspec (~> 1.5.0) rubocop-rspec (~> 1.5.0)
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2) ruby-prof (~> 0.16.2)
...@@ -948,7 +951,7 @@ DEPENDENCIES ...@@ -948,7 +951,7 @@ DEPENDENCIES
sdoc (~> 0.3.20) sdoc (~> 0.3.20)
seed-fu (~> 2.3.5) seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9) select2-rails (~> 3.5.9)
sentry-raven (~> 1.1.0) sentry-raven (~> 2.0.0)
settingslogic (~> 2.0.9) settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6) sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0) shoulda-matchers (~> 2.8.0)
...@@ -971,6 +974,7 @@ DEPENDENCIES ...@@ -971,6 +974,7 @@ DEPENDENCIES
test_after_commit (~> 0.4.2) test_after_commit (~> 0.4.2)
thin (~> 1.7.0) thin (~> 1.7.0)
timecop (~> 0.8.0) timecop (~> 0.8.0)
truncato (~> 0.7.8)
turbolinks (~> 2.5.0) turbolinks (~> 2.5.0)
u2f (~> 0.2.1) u2f (~> 0.2.1)
uglifier (~> 2.7.2) uglifier (~> 2.7.2)
...@@ -986,4 +990,4 @@ DEPENDENCIES ...@@ -986,4 +990,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.13.1 1.13.2
(function() { ((global) => {
this.LabelManager = (function() {
LabelManager.prototype.errorMessage = 'Unable to update label prioritization at this time';
function LabelManager(opts) { class LabelManager {
// Defaults constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
var ref, ref1, ref2; this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority');
if (opts == null) { this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels');
opts = {}; this.otherLabels = otherLabels || $('.js-other-labels');
} this.errorMessage = 'Unable to update label prioritization at this time';
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({ this.prioritizedLabels.sortable({
items: 'li', items: 'li',
placeholder: 'list-placeholder', placeholder: 'list-placeholder',
...@@ -18,33 +15,30 @@ ...@@ -18,33 +15,30 @@
this.bindEvents(); this.bindEvents();
} }
LabelManager.prototype.bindEvents = function() { bindEvents() {
return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick); return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
}; }
LabelManager.prototype.onTogglePriorityClick = function(e) { onTogglePriorityClick(e) {
var $btn, $label, $tooltip, _this, action;
e.preventDefault(); e.preventDefault();
_this = e.data; const _this = e.data;
$btn = $(e.currentTarget); const $btn = $(e.currentTarget);
$label = $("#" + ($btn.data('domId'))); const $label = $(`#${$btn.data('domId')}`);
action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
// Make sure tooltip will hide const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`);
$tooltip = $("#" + ($btn.find('.has-tooltip:visible').attr('aria-describedby')));
$tooltip.tooltip('destroy'); $tooltip.tooltip('destroy');
return _this.toggleLabelPriority($label, action); return _this.toggleLabelPriority($label, action);
}; }
LabelManager.prototype.toggleLabelPriority = function($label, action, persistState) { toggleLabelPriority($label, action, persistState) {
var $from, $target, _this, url, xhr;
if (persistState == null) { if (persistState == null) {
persistState = true; persistState = true;
} }
_this = this; let xhr;
url = $label.find('.js-toggle-priority').data('url'); const _this = this;
$target = this.prioritizedLabels; const url = $label.find('.js-toggle-priority').data('url');
$from = this.otherLabels; let $target = this.prioritizedLabels;
// Optimistic update let $from = this.otherLabels;
if (action === 'remove') { if (action === 'remove') {
$target = this.otherLabels; $target = this.otherLabels;
$from = this.prioritizedLabels; $from = this.prioritizedLabels;
...@@ -62,7 +56,7 @@ ...@@ -62,7 +56,7 @@
} }
if (action === 'remove') { if (action === 'remove') {
xhr = $.ajax({ xhr = $.ajax({
url: url, url,
type: 'DELETE' type: 'DELETE'
}); });
// Restore empty message // Restore empty message
...@@ -73,43 +67,40 @@ ...@@ -73,43 +67,40 @@
xhr = this.savePrioritySort($label, action); xhr = this.savePrioritySort($label, action);
} }
return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action)); return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
}; }
LabelManager.prototype.onPrioritySortUpdate = function() { onPrioritySortUpdate() {
var xhr; const xhr = this.savePrioritySort();
xhr = this.savePrioritySort();
return xhr.fail(function() { return xhr.fail(function() {
return new Flash(this.errorMessage, 'alert'); return new Flash(this.errorMessage, 'alert');
}); });
}; }
LabelManager.prototype.savePrioritySort = function() { savePrioritySort() {
return $.post({ return $.post({
url: this.prioritizedLabels.data('url'), url: this.prioritizedLabels.data('url'),
data: { data: {
label_ids: this.getSortedLabelsIds() label_ids: this.getSortedLabelsIds()
} }
}); });
}; }
LabelManager.prototype.rollbackLabelPosition = function($label, originalAction) { rollbackLabelPosition($label, originalAction) {
var action; const action = originalAction === 'remove' ? 'add' : 'remove';
action = originalAction === 'remove' ? 'add' : 'remove';
this.toggleLabelPriority($label, action, false); this.toggleLabelPriority($label, action, false);
return new Flash(this.errorMessage, 'alert'); return new Flash(this.errorMessage, 'alert');
}; }
LabelManager.prototype.getSortedLabelsIds = function() { getSortedLabelsIds() {
var sortedIds; const sortedIds = [];
sortedIds = [];
this.prioritizedLabels.find('li').each(function() { this.prioritizedLabels.find('li').each(function() {
return sortedIds.push($(this).data('id')); sortedIds.push($(this).data('id'));
}); });
return sortedIds; return sortedIds;
}; }
}
return LabelManager; gl.LabelManager = LabelManager;
})(); })(window.gl || (window.gl = {}));
}).call(this);
...@@ -21,16 +21,14 @@ ...@@ -21,16 +21,14 @@
}; };
Activities.prototype.toggleFilter = function(sender) { Activities.prototype.toggleFilter = function(sender) {
var event_filters, filter; var filter = sender.attr("id").split("_")[0];
$('.event-filter .active').removeClass("active"); $('.event-filter .active').removeClass("active");
event_filters = $.cookie("event_filter"); $.cookie("event_filter", filter, {
filter = sender.attr("id").split("_")[0];
$.cookie("event_filter", (event_filters !== filter ? filter : ""), {
path: gon.relative_url_root || '/' path: gon.relative_url_root || '/'
}); });
if (event_filters !== filter) {
return sender.closest('li').toggleClass("active"); sender.closest('li').toggleClass("active");
}
}; };
return Activities; return Activities;
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
namespacesPath: "/api/:version/namespaces.json", namespacesPath: "/api/:version/namespaces.json",
groupProjectsPath: "/api/:version/groups/:id/projects.json", groupProjectsPath: "/api/:version/groups/:id/projects.json",
projectsPath: "/api/:version/projects.json?simple=true", projectsPath: "/api/:version/projects.json?simple=true",
labelsPath: "/api/:version/projects/:id/labels", labelsPath: "/:namespace_path/:project_path/labels",
licensePath: "/api/:version/licenses/:key", licensePath: "/api/:version/licenses/:key",
gitignorePath: "/api/:version/gitignores/:key", gitignorePath: "/api/:version/gitignores/:key",
gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key", gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
...@@ -23,12 +23,13 @@ ...@@ -23,12 +23,13 @@
}, },
// Return groups list. Filtered by query // Return groups list. Filtered by query
// Only active groups retrieved // Only active groups retrieved
groups: function(query, skip_ldap, callback) { groups: function(query, skip_ldap, skip_groups, callback) {
var url = Api.buildUrl(Api.groupsPath); var url = Api.buildUrl(Api.groupsPath);
return $.ajax({ return $.ajax({
url: url, url: url,
data: { data: {
search: query, search: query,
skip_groups: skip_groups,
per_page: 20 per_page: 20
}, },
dataType: "json" dataType: "json"
...@@ -65,13 +66,14 @@ ...@@ -65,13 +66,14 @@
return callback(projects); return callback(projects);
}); });
}, },
newLabel: function(project_id, data, callback) { newLabel: function(namespace_path, project_path, data, callback) {
var url = Api.buildUrl(Api.labelsPath) var url = Api.buildUrl(Api.labelsPath)
.replace(':id', project_id); .replace(':namespace_path', namespace_path)
.replace(':project_path', project_path);
return $.ajax({ return $.ajax({
url: url, url: url,
type: "POST", type: "POST",
data: data, data: {'label': data},
dataType: "json" dataType: "json"
}).done(function(label) { }).done(function(label) {
return callback(label); return callback(label);
......
/*= 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);
/*= require blob/template_selector */
((global) => {
class BlobCiYamlSelector extends gl.TemplateSelector {
requestFile(query) {
return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
}
requestFileSuccess(file) {
return super.requestFileSuccess(file);
}
}
global.BlobCiYamlSelector = BlobCiYamlSelector;
class BlobCiYamlSelectors {
constructor({ editor, $dropdowns } = {}) {
this.editor = editor;
this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector');
this.initSelectors();
}
initSelectors() {
const editor = this.editor;
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
return new BlobCiYamlSelector({
editor,
pattern: /(.gitlab-ci.yml)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
dropdown: $dropdown
});
});
}
}
global.BlobCiYamlSelectors = BlobCiYamlSelectors;
})(window.gl || (window.gl = {}));
...@@ -18,6 +18,6 @@ ...@@ -18,6 +18,6 @@
return BlobGitignoreSelector; return BlobGitignoreSelector;
})(TemplateSelector); })(gl.TemplateSelector);
}).call(this); }).call(this);
...@@ -23,6 +23,6 @@ ...@@ -23,6 +23,6 @@
return BlobLicenseSelector; return BlobLicenseSelector;
})(TemplateSelector); })(gl.TemplateSelector);
}).call(this); }).call(this);
(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);
((global) => {
class BlobLicenseSelectors {
constructor({ $dropdowns, editor }) {
this.$dropdowns = $('.js-license-selector');
this.editor = editor;
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
return new BlobLicenseSelector({
editor,
pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-license-selector-wrap'),
dropdown: $dropdown,
});
});
}
}
global.BlobLicenseSelectors = BlobLicenseSelectors;
})(window.gl || (window.gl = {}));
(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.dropdownIcon = $('.fa-chevron-down', this.dropdown);
this.buildDropdown();
this.bindEvents();
this.onFilenameUpdate();
this.autosizeUpdateEvent = document.createEvent('Event');
this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
}
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) {
// This `requestFile` method is an abstract method that should
// be added by all subclasses.
};
// To be implemented on the extending class
// e.g.
// Api.gitignoreText item.name, @requestFileSuccess.bind(@)
TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) {
this.editor.setValue(file.content, 1);
if (!skipFocus) this.editor.focus();
if (this.editor instanceof jQuery) {
this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
}
};
TemplateSelector.prototype.startLoadingSpinner = function() {
this.dropdownIcon
.addClass('fa-spinner fa-spin')
.removeClass('fa-chevron-down');
};
TemplateSelector.prototype.stopLoadingSpinner = function() {
this.dropdownIcon
.addClass('fa-chevron-down')
.removeClass('fa-spinner fa-spin');
};
return TemplateSelector;
})();
}).call(this);
((global) => {
class TemplateSelector {
constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) {
this.onClick = this.onClick.bind(this);
this.dropdown = dropdown;
this.data = data;
this.pattern = pattern;
this.wrapper = wrapper;
this.editor = editor;
this.fileEndpoint = fileEndpoint;
this.$input = $input || $('#file_name');
this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
this.buildDropdown();
this.bindEvents();
this.onFilenameUpdate();
this.autosizeUpdateEvent = document.createEvent('Event');
this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
}
buildDropdown() {
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;
}
});
}
bindEvents() {
return this.$input.on('keyup blur', (e) => this.onFilenameUpdate());
}
toggleLabel(item) {
return item.name;
}
onFilenameUpdate() {
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');
}
onClick(item, el, e) {
e.preventDefault();
return this.requestFile(item);
}
requestFile(item) {
// This `requestFile` method is an abstract method that should
// be added by all subclasses.
}
// To be implemented on the extending class
// e.g.
// Api.gitignoreText item.name, @requestFileSuccess.bind(@)
requestFileSuccess(file, { skipFocus, append } = {}) {
const oldValue = this.editor.getValue();
let newValue = file.content;
if (append && oldValue.length && oldValue !== newValue) {
newValue = oldValue + '\n\n' + newValue;
}
this.editor.setValue(newValue, 1);
if (!skipFocus) this.editor.focus();
if (this.editor instanceof jQuery) {
this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
}
}
startLoadingSpinner() {
this.dropdownIcon
.addClass('fa-spinner fa-spin')
.removeClass('fa-chevron-down');
}
stopLoadingSpinner() {
this.dropdownIcon
.addClass('fa-chevron-down')
.removeClass('fa-spinner fa-spin');
}
}
global.TemplateSelector = TemplateSelector;
})(window.gl || ( window.gl = {}));
...@@ -23,13 +23,13 @@ ...@@ -23,13 +23,13 @@
})(this)); })(this));
this.initModePanesAndLinks(); this.initModePanesAndLinks();
this.initSoftWrap(); this.initSoftWrap();
new BlobLicenseSelectors({ new gl.BlobLicenseSelectors({
editor: this.editor editor: this.editor
}); });
new BlobGitignoreSelectors({ new BlobGitignoreSelectors({
editor: this.editor editor: this.editor
}); });
new BlobCiYamlSelectors({ new gl.BlobCiYamlSelectors({
editor: this.editor editor: this.editor
}); });
} }
......
...@@ -21,7 +21,8 @@ ...@@ -21,7 +21,8 @@
}, },
data () { data () {
return { return {
filters: Store.state.filters filters: Store.state.filters,
showIssueForm: false
}; };
}, },
watch: { watch: {
...@@ -33,6 +34,11 @@ ...@@ -33,6 +34,11 @@
deep: true deep: true
} }
}, },
methods: {
showNewIssueForm() {
this.showIssueForm = !this.showIssueForm;
}
},
ready () { ready () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({ const options = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled, disabled: this.disabled,
......
...@@ -8,10 +8,8 @@ ...@@ -8,10 +8,8 @@
data () { data () {
return { return {
predefinedLabels: [ predefinedLabels: [
new ListLabel({ title: 'Development', color: '#5CB85C' }), new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
new ListLabel({ title: 'Testing', color: '#F0AD4E' }), new ListLabel({ title: 'Doing', color: '#5CB85C' })
new ListLabel({ title: 'Production', color: '#FF5F00' }),
new ListLabel({ title: 'Ready', color: '#FF0000' })
] ]
} }
}, },
......
//= require ./board_card //= require ./board_card
//= require ./board_new_issue
(() => { (() => {
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
...@@ -8,14 +9,16 @@ ...@@ -8,14 +9,16 @@
gl.issueBoards.BoardList = Vue.extend({ gl.issueBoards.BoardList = Vue.extend({
components: { components: {
'board-card': gl.issueBoards.BoardCard 'board-card': gl.issueBoards.BoardCard,
'board-new-issue': gl.issueBoards.BoardNewIssue
}, },
props: { props: {
disabled: Boolean, disabled: Boolean,
list: Object, list: Object,
issues: Array, issues: Array,
loading: Boolean, loading: Boolean,
issueLinkBase: String issueLinkBase: String,
showIssueForm: Boolean
}, },
data () { data () {
return { return {
...@@ -73,7 +76,7 @@ ...@@ -73,7 +76,7 @@
group: 'issues', group: 'issues',
sort: false, sort: false,
disabled: this.disabled, disabled: this.disabled,
filter: '.board-list-count', filter: '.board-list-count, .is-disabled',
onStart: (e) => { onStart: (e) => {
const card = this.$refs.issue[e.oldIndex]; const card = this.$refs.issue[e.oldIndex];
......
(() => {
window.gl = window.gl || {};
gl.issueBoards.BoardNewIssue = Vue.extend({
props: {
list: Object,
showIssueForm: Boolean
},
data() {
return {
title: '',
error: false
};
},
watch: {
showIssueForm () {
this.$els.input.focus();
}
},
methods: {
submit(e) {
e.preventDefault();
if (this.title.trim() === '') return;
this.error = false;
const labels = this.list.label ? [this.list.label] : [];
const issue = new ListIssue({
title: this.title,
labels
});
this.list.newIssue(issue)
.then((data) => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$els.submitButton).enable();
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$els.submitButton).enable();
// Remove the issue
this.list.removeIssue(issue);
// Show error message
this.error = true;
this.showIssueForm = true;
});
this.cancel();
},
cancel() {
this.showIssueForm = false;
this.title = '';
}
}
});
})();
...@@ -3,8 +3,7 @@ $(() => { ...@@ -3,8 +3,7 @@ $(() => {
$('.js-new-board-list').each(function () { $('.js-new-board-list').each(function () {
const $this = $(this); const $this = $(this);
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('project-id'));
$this.glDropdown({ $this.glDropdown({
data(term, callback) { data(term, callback) {
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
fallbackClass: 'is-dragging', fallbackClass: 'is-dragging',
fallbackOnBody: true, fallbackOnBody: true,
ghostClass: 'is-ghost', ghostClass: 'is-ghost',
filter: '.has-tooltip', filter: '.has-tooltip, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 0, delay: gl.issueBoards.touchEnabled ? 100 : 0,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20, scrollSpeed: 20,
......
...@@ -87,6 +87,17 @@ class List { ...@@ -87,6 +87,17 @@ class List {
}); });
} }
newIssue (issue) {
this.addIssue(issue);
this.issuesSize++;
return gl.boardService.newIssue(this.id, issue)
.then((resp) => {
const data = resp.json();
issue.id = data.iid;
});
}
createIssues (data) { createIssues (data) {
data.forEach((issueObj) => { data.forEach((issueObj) => {
this.addIssue(new ListIssue(issueObj)); this.addIssue(new ListIssue(issueObj));
......
...@@ -58,4 +58,10 @@ class BoardService { ...@@ -58,4 +58,10 @@ class BoardService {
to_list_id to_list_id
}); });
} }
newIssue (id, issue) {
return this.issues.save({ id }, {
issue
});
}
}; };
...@@ -146,7 +146,7 @@ ...@@ -146,7 +146,7 @@
$date = $('.js-artifacts-remove'); $date = $('.js-artifacts-remove');
if ($date.length) { if ($date.length) {
date = $date.text(); date = $date.text();
return $date.text($.timefor(new Date(date.replace(/-/g, '/')), ' ')); return $date.text($.timefor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
} }
}; };
......
...@@ -26,15 +26,15 @@ ...@@ -26,15 +26,15 @@
}; };
showTooltip = function(target, title) { showTooltip = function(target, title) {
return $(target).tooltip({ var $target = $(target);
container: 'body', var originalTitle = $target.data('original-title');
html: 'true',
placement: 'auto bottom', $target
title: title, .attr('title', 'Copied!')
trigger: 'manual' .tooltip('fixTitle')
}).tooltip('show').one('mouseleave', function() { .tooltip('show')
return $(this).tooltip('hide'); .attr('title', originalTitle)
}); .tooltip('fixTitle');
}; };
$(function() { $(function() {
......
(function (w) { (function (w) {
class CreateLabelDropdown { class CreateLabelDropdown {
constructor ($el, projectId) { constructor ($el, namespacePath, projectPath) {
this.$el = $el; this.$el = $el;
this.projectId = projectId; this.namespacePath = namespacePath;
this.projectPath = projectPath;
this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown')); this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
this.$cancelButton = $('.js-cancel-label-btn', this.$el); this.$cancelButton = $('.js-cancel-label-btn', this.$el);
this.$newLabelField = $('#new_label_name', this.$el); this.$newLabelField = $('#new_label_name', this.$el);
...@@ -91,8 +92,8 @@ ...@@ -91,8 +92,8 @@
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
Api.newLabel(this.projectId, { Api.newLabel(this.namespacePath, this.projectPath, {
name: this.$newLabelField.val(), title: this.$newLabelField.val(),
color: this.$newColorField.val() color: this.$newColorField.val()
}, (label) => { }, (label) => {
this.$newLabelCreateButton.enable(); this.$newLabelCreateButton.enable();
......
...@@ -7,6 +7,9 @@ ...@@ -7,6 +7,9 @@
function Diff() { function Diff() {
$('.files .diff-file').singleFileDiff(); $('.files .diff-file').singleFileDiff();
this.filesCommentButton = $('.files .diff-file').filesCommentButton(); this.filesCommentButton = $('.files .diff-file').filesCommentButton();
if (this.diffViewType() === 'parallel') {
$('.content-wrapper .container-fluid').removeClass('container-limited');
}
$(document).off('click', '.js-unfold'); $(document).off('click', '.js-unfold');
$(document).on('click', '.js-unfold', (function(_this) { $(document).on('click', '.js-unfold', (function(_this) {
return function(event) { return function(event) {
...@@ -52,6 +55,10 @@ ...@@ -52,6 +55,10 @@
})(this)); })(this));
} }
Diff.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type');
}
Diff.prototype.lineNumbers = function(line) { Diff.prototype.lineNumbers = function(line) {
if (!line.children().length) { if (!line.children().length) {
return [0, 0]; return [0, 0];
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
case 'projects:merge_requests:index': case 'projects:merge_requests:index':
case 'projects:issues:index': case 'projects:issues:index':
Issuable.init(); Issuable.init();
new IssuableBulkActions(); new gl.IssuableBulkActions();
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
break; break;
case 'projects:issues:show': case 'projects:issues:show':
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
new Milestone(); new Milestone();
break; break;
case 'dashboard:todos:index': case 'dashboard:todos:index':
new Todos(); new gl.Todos();
break; break;
case 'projects:milestones:new': case 'projects:milestones:new':
case 'projects:milestones:edit': case 'projects:milestones:edit':
...@@ -59,7 +59,9 @@ ...@@ -59,7 +59,9 @@
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new GLForm($('.issue-form')); new GLForm($('.issue-form'));
new IssuableForm($('.issue-form')); new IssuableForm($('.issue-form'));
new IssuableTemplateSelectors(); new LabelsSelect();
new MilestoneSelect();
new gl.IssuableTemplateSelectors();
break; break;
case 'projects:merge_requests:new': case 'projects:merge_requests:new':
case 'projects:merge_requests:edit': case 'projects:merge_requests:edit':
...@@ -67,7 +69,9 @@ ...@@ -67,7 +69,9 @@
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new GLForm($('.merge-request-form')); new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form')); new IssuableForm($('.merge-request-form'));
new IssuableTemplateSelectors(); new LabelsSelect();
new MilestoneSelect();
new gl.IssuableTemplateSelectors();
break; break;
case 'projects:tags:new': case 'projects:tags:new':
new ZenMode(); new ZenMode();
...@@ -165,7 +169,7 @@ ...@@ -165,7 +169,7 @@
break; break;
case 'projects:labels:index': case 'projects:labels:index':
if ($('.prioritized-labels').length) { if ($('.prioritized-labels').length) {
new LabelManager(); new gl.LabelManager();
} }
break; break;
case 'projects:network:show': case 'projects:network:show':
...@@ -279,7 +283,7 @@ ...@@ -279,7 +283,7 @@
Dispatcher.prototype.initSearch = function() { Dispatcher.prototype.initSearch = function() {
// Only when search form is present // Only when search form is present
if ($('.search').length) { if ($('.search').length) {
return new SearchAutocomplete(); return new gl.SearchAutocomplete();
} }
}; };
......
...@@ -443,6 +443,7 @@ ...@@ -443,6 +443,7 @@
var contentHtml; var contentHtml;
this.resetRows(); this.resetRows();
this.addArrowKeyEvent(); this.addArrowKeyEvent();
if (this.options.setIndeterminateIds) { if (this.options.setIndeterminateIds) {
this.options.setIndeterminateIds.call(this); this.options.setIndeterminateIds.call(this);
} }
...@@ -460,9 +461,21 @@ ...@@ -460,9 +461,21 @@
if (this.options.filterable) { if (this.options.filterable) {
this.filterInput.focus(); this.filterInput.focus();
} }
if (this.options.showMenuAbove) {
this.positionMenuAbove();
}
return this.dropdown.trigger('shown.gl.dropdown'); return this.dropdown.trigger('shown.gl.dropdown');
}; };
GitLabDropdown.prototype.positionMenuAbove = function() {
var $button = $(this.el);
var $menu = this.dropdown.find('.dropdown-menu');
$menu.css('top', ($button.height() + $menu.height()) * -1);
};
GitLabDropdown.prototype.hidden = function(e) { GitLabDropdown.prototype.hidden = function(e) {
var $input; var $input;
this.resetRows(); this.resetRows();
......
...@@ -5,14 +5,15 @@ ...@@ -5,14 +5,15 @@
function GroupsSelect() { function GroupsSelect() {
$('.ajax-groups-select').each((function(_this) { $('.ajax-groups-select').each((function(_this) {
return function(i, select) { return function(i, select) {
var skip_ldap; var skip_ldap, skip_groups;
skip_ldap = $(select).hasClass('skip_ldap'); skip_ldap = $(select).hasClass('skip_ldap');
skip_groups = $(select).data('skip-groups') || [];
return $(select).select2({ return $(select).select2({
placeholder: "Search for a group", placeholder: "Search for a group",
multiple: $(select).hasClass('multiselect'), multiple: $(select).hasClass('multiselect'),
minimumInputLength: 0, minimumInputLength: 0,
query: function(query) { query: function(query) {
return Api.groups(query.term, skip_ldap, function(groups) { return Api.groups(query.term, skip_ldap, skip_groups, function(groups) {
var data; var data;
data = { data = {
results: groups results: groups
......
...@@ -51,7 +51,6 @@ ...@@ -51,7 +51,6 @@
}).remove(); }).remove();
// Submit the form to get new data // Submit the form to get new data
Issuable.filterResults($('.filter-form')); Issuable.filterResults($('.filter-form'));
return $('.js-label-select').trigger('update.label');
}); });
}, },
filterResults: (function(_this) { filterResults: (function(_this) {
......
(function() { ((global) => {
this.IssuableBulkActions = (function() {
function IssuableBulkActions(opts) { class IssuableBulkActions {
// Set defaults constructor({ container, form, issues } = {}) {
var ref, ref1, ref2; this.container = container || $('.content'),
if (opts == null) { this.form = form || this.getElement('.bulk-update');
opts = {}; this.issues = issues || this.getElement('.issues-list .issue');
}
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('.issuable-list > li');
// Save instance
this.form.data('bulkActions', this); this.form.data('bulkActions', this);
this.willUpdateLabels = false; this.willUpdateLabels = false;
this.bindEvents(); this.bindEvents();
...@@ -15,53 +12,46 @@ ...@@ -15,53 +12,46 @@
Issuable.initChecks(); Issuable.initChecks();
} }
IssuableBulkActions.prototype.getElement = function(selector) { getElement(selector) {
return this.container.find(selector); return this.container.find(selector);
}; }
IssuableBulkActions.prototype.bindEvents = function() { bindEvents() {
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this)); return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
}; }
IssuableBulkActions.prototype.onFormSubmit = function(e) { onFormSubmit(e) {
e.preventDefault(); e.preventDefault();
return this.submit(); return this.submit();
}; }
IssuableBulkActions.prototype.submit = function() { submit() {
var _this, xhr; const _this = this;
_this = this; const xhr = $.ajax({
xhr = $.ajax({
url: this.form.attr('action'), url: this.form.attr('action'),
method: this.form.attr('method'), method: this.form.attr('method'),
dataType: 'JSON', dataType: 'JSON',
data: this.getFormDataAsObject() data: this.getFormDataAsObject()
}); });
xhr.done(function(response, status, xhr) { xhr.done(() => window.location.reload());
return location.reload(); xhr.fail(() => new Flash("Issue update failed"));
});
xhr.fail(function() {
return new Flash("Issue update failed");
});
return xhr.always(this.onFormSubmitAlways.bind(this)); return xhr.always(this.onFormSubmitAlways.bind(this));
}; }
IssuableBulkActions.prototype.onFormSubmitAlways = function() { onFormSubmitAlways() {
return this.form.find('[type="submit"]').enable(); return this.form.find('[type="submit"]').enable();
}; }
IssuableBulkActions.prototype.getSelectedIssues = function() { getSelectedIssues() {
return this.issues.has('.selected_issue:checked'); return this.issues.has('.selected_issue:checked');
}; }
IssuableBulkActions.prototype.getLabelsFromSelection = function() { getLabelsFromSelection() {
var labels; const labels = [];
labels = [];
this.getSelectedIssues().map(function() { this.getSelectedIssues().map(function() {
var _labels; const labelsData = $(this).data('labels');
_labels = $(this).data('labels'); if (labelsData) {
if (_labels) { return labelsData.map(function(labelId) {
return _labels.map(function(labelId) {
if (labels.indexOf(labelId) === -1) { if (labels.indexOf(labelId) === -1) {
return labels.push(labelId); return labels.push(labelId);
} }
...@@ -69,7 +59,7 @@ ...@@ -69,7 +59,7 @@
} }
}); });
return labels; return labels;
}; }
/** /**
...@@ -77,25 +67,21 @@ ...@@ -77,25 +67,21 @@
* @return {Array} Label IDs * @return {Array} Label IDs
*/ */
IssuableBulkActions.prototype.getUnmarkedIndeterminedLabels = function() { getUnmarkedIndeterminedLabels() {
var el, i, id, j, labelsToKeep, len, len1, ref, ref1, result; const result = [];
result = []; const labelsToKeep = [];
labelsToKeep = [];
ref = this.getElement('.labels-filter .is-indeterminate'); this.getElement('.labels-filter .is-indeterminate')
for (i = 0, len = ref.length; i < len; i++) { .each((i, el) => labelsToKeep.push($(el).data('labelId')));
el = ref[i];
labelsToKeep.push($(el).data('labelId')); this.getLabelsFromSelection().forEach((id) => {
}
ref1 = this.getLabelsFromSelection();
for (j = 0, len1 = ref1.length; j < len1; j++) {
id = ref1[j];
// Only the ones that we are not going to keep
if (labelsToKeep.indexOf(id) === -1) { if (labelsToKeep.indexOf(id) === -1) {
result.push(id); result.push(id);
} }
} });
return result; return result;
}; }
/** /**
...@@ -103,9 +89,8 @@ ...@@ -103,9 +89,8 @@
* Returns key/value pairs from form data * Returns key/value pairs from form data
*/ */
IssuableBulkActions.prototype.getFormDataAsObject = function() { getFormDataAsObject() {
var formData; const formData = {
formData = {
update: { update: {
state_event: this.form.find('input[name="update[state_event]"]').val(), state_event: this.form.find('input[name="update[state_event]"]').val(),
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(), assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
...@@ -125,19 +110,18 @@ ...@@ -125,19 +110,18 @@
}); });
} }
return formData; return formData;
}; }
IssuableBulkActions.prototype.getLabelsToApply = function() { getLabelsToApply() {
var $labels, labelIds; const labelIds = [];
labelIds = []; const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
$labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
$labels.each(function(k, label) { $labels.each(function(k, label) {
if (label) { if (label) {
return labelIds.push(parseInt($(label).val())); return labelIds.push(parseInt($(label).val()));
} }
}); });
return labelIds; return labelIds;
}; }
/** /**
...@@ -145,11 +129,10 @@ ...@@ -145,11 +129,10 @@
* @return {Array} Array of labels IDs * @return {Array} Array of labels IDs
*/ */
IssuableBulkActions.prototype.getLabelsToRemove = function() { getLabelsToRemove() {
var indeterminatedLabels, labelsToApply, result; const result = [];
result = []; const indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
indeterminatedLabels = this.getUnmarkedIndeterminedLabels(); const labelsToApply = this.getLabelsToApply();
labelsToApply = this.getLabelsToApply();
indeterminatedLabels.map(function(id) { indeterminatedLabels.map(function(id) {
// We need to exclude label IDs that will be applied // 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 // By not doing this will cause issues from selection to not add labels at all
...@@ -158,10 +141,9 @@ ...@@ -158,10 +141,9 @@
} }
}); });
return result; return result;
}; }
}
return IssuableBulkActions;
})(); global.IssuableBulkActions = IssuableBulkActions;
}).call(this); })(window.gl || (window.gl = {}));
This diff is collapsed.
...@@ -38,6 +38,11 @@ ...@@ -38,6 +38,11 @@
gl.utils.getPagePath = function() { gl.utils.getPagePath = function() {
return $('body').data('page').split(':')[0]; return $('body').data('page').split(':')[0];
}; };
gl.utils.parseUrl = function (url) {
var parser = document.createElement('a');
parser.href = url;
return parser;
};
return jQuery.timefor = function(time, suffix, expiredLabel) { return jQuery.timefor = function(time, suffix, expiredLabel) {
var suffixFromNow, timefor; var suffixFromNow, timefor;
if (!time) { if (!time) {
......
...@@ -7,13 +7,16 @@ const ORIGIN_BUTTON_TITLE = 'Use theirs'; ...@@ -7,13 +7,16 @@ const ORIGIN_BUTTON_TITLE = 'Use theirs';
class MergeConflictDataProvider { class MergeConflictDataProvider {
getInitialData() { getInitialData() {
// TODO: remove reliance on jQuery and DOM state introspection
const diffViewType = $.cookie('diff_view'); const diffViewType = $.cookie('diff_view');
const fixedLayout = $('.content-wrapper .container-fluid').hasClass('container-limited');
return { return {
isLoading : true, isLoading : true,
hasError : false, hasError : false,
isParallel : diffViewType === 'parallel', isParallel : diffViewType === 'parallel',
diffViewType : diffViewType, diffViewType : diffViewType,
fixedLayout : fixedLayout,
isSubmitting : false, isSubmitting : false,
conflictsData : {}, conflictsData : {},
resolutionData : {} resolutionData : {}
...@@ -192,14 +195,17 @@ class MergeConflictDataProvider { ...@@ -192,14 +195,17 @@ class MergeConflictDataProvider {
updateViewType(newType) { updateViewType(newType) {
const vi = this.vueInstance; const vi = this.vueInstance;
if (newType === vi.diffView || !(newType === 'parallel' || newType === 'inline')) { if (newType === vi.diffViewType || !(newType === 'parallel' || newType === 'inline')) {
return; return;
} }
vi.diffView = newType; vi.diffViewType = newType;
vi.isParallel = newType === 'parallel'; vi.isParallel = newType === 'parallel';
$.cookie('diff_view', newType); // TODO: Make sure that cookie path added. $.cookie('diff_view', newType, {
$('.content-wrapper .container-fluid').toggleClass('container-limited'); path: (gon && gon.relative_url_root) || '/'
});
$('.content-wrapper .container-fluid')
.toggleClass('container-limited', !vi.isParallel && vi.fixedLayout);
} }
......
...@@ -60,9 +60,8 @@ class MergeConflictResolver { ...@@ -60,9 +60,8 @@ class MergeConflictResolver {
$('#conflicts .js-syntax-highlight').syntaxHighlight(); $('#conflicts .js-syntax-highlight').syntaxHighlight();
}); });
if (this.vue.diffViewType === 'parallel') { $('.content-wrapper .container-fluid')
$('.content-wrapper .container-fluid').removeClass('container-limited'); .toggleClass('container-limited', !this.vue.isParallel && this.vue.fixedLayout);
}
}) })
} }
......
...@@ -36,13 +36,10 @@ ...@@ -36,13 +36,10 @@
}; };
MergeRequest.prototype.initTabs = function() { MergeRequest.prototype.initTabs = function() {
if (this.opts.action !== 'new') { if (window.mrTabs) {
// `MergeRequests#new` has no tab-persisting or lazy-loading behavior window.mrTabs.unbindEvents();
window.mrTabs = new MergeRequestTabs(this.opts);
} else {
// Show the first tab (Commits)
return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show');
} }
window.mrTabs = new MergeRequestTabs(this.opts);
}; };
MergeRequest.prototype.showAllCommits = function() { MergeRequest.prototype.showAllCommits = function() {
......
...@@ -56,9 +56,14 @@ ...@@ -56,9 +56,14 @@
MergeRequestTabs.prototype.commitsLoaded = false; MergeRequestTabs.prototype.commitsLoaded = false;
MergeRequestTabs.prototype.fixedLayoutPref = null;
function MergeRequestTabs(opts) { function MergeRequestTabs(opts) {
this.opts = opts != null ? opts : {}; this.opts = opts != null ? opts : {};
this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true; this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
this.buildsLoaded = this.opts.buildsLoaded || false;
this.setCurrentAction = bind(this.setCurrentAction, this); this.setCurrentAction = bind(this.setCurrentAction, this);
this.tabShown = bind(this.tabShown, this); this.tabShown = bind(this.tabShown, this);
this.showTab = bind(this.showTab, this); this.showTab = bind(this.showTab, this);
...@@ -70,7 +75,12 @@ ...@@ -70,7 +75,12 @@
MergeRequestTabs.prototype.bindEvents = function() { MergeRequestTabs.prototype.bindEvents = function() {
$(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); $(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
return $(document).on('click', '.js-show-tab', this.showTab); $(document).on('click', '.js-show-tab', this.showTab);
};
MergeRequestTabs.prototype.unbindEvents = function() {
$(document).off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
$(document).off('click', '.js-show-tab', this.showTab);
}; };
MergeRequestTabs.prototype.showTab = function(event) { MergeRequestTabs.prototype.showTab = function(event) {
...@@ -85,11 +95,15 @@ ...@@ -85,11 +95,15 @@
if (action === 'commits') { if (action === 'commits') {
this.loadCommits($target.attr('href')); this.loadCommits($target.attr('href'));
this.expandView(); this.expandView();
} else if (action === 'diffs') { this.resetViewContainer();
} else if (this.isDiffAction(action)) {
this.loadDiff($target.attr('href')); this.loadDiff($target.attr('href'));
if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') { if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
this.shrinkView(); this.shrinkView();
} }
if (this.diffViewType() === 'parallel') {
this.expandViewContainer();
}
navBarHeight = $('.navbar-gitlab').outerHeight(); navBarHeight = $('.navbar-gitlab').outerHeight();
$.scrollTo(".merge-request-details .merge-request-tabs", { $.scrollTo(".merge-request-details .merge-request-tabs", {
offset: -navBarHeight offset: -navBarHeight
...@@ -97,11 +111,14 @@ ...@@ -97,11 +111,14 @@
} else if (action === 'builds') { } else if (action === 'builds') {
this.loadBuilds($target.attr('href')); this.loadBuilds($target.attr('href'));
this.expandView(); this.expandView();
this.resetViewContainer();
} else if (action === 'pipelines') { } else if (action === 'pipelines') {
this.loadPipelines($target.attr('href')); this.loadPipelines($target.attr('href'));
this.expandView(); this.expandView();
this.resetViewContainer();
} else { } else {
this.expandView(); this.expandView();
this.resetViewContainer();
} }
if (this.opts.setUrl) { if (this.opts.setUrl) {
this.setCurrentAction(action); this.setCurrentAction(action);
...@@ -126,7 +143,7 @@ ...@@ -126,7 +143,7 @@
if (action === 'show') { if (action === 'show') {
action = 'notes'; action = 'notes';
} }
return $(".merge-request-tabs a[data-action='" + action + "']").tab('show'); $(".merge-request-tabs a[data-action='" + action + "']").tab('show').trigger('shown.bs.tab');
}; };
// Replaces the current Merge Request-specific action in the URL with a new one // Replaces the current Merge Request-specific action in the URL with a new one
...@@ -156,8 +173,9 @@ ...@@ -156,8 +173,9 @@
action = 'notes'; action = 'notes';
} }
this.currentAction = action; this.currentAction = action;
// Remove a trailing '/commits' or '/diffs' // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, ''); new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
// Append the new action if we're on a tab other than 'notes' // Append the new action if we're on a tab other than 'notes'
if (action !== 'notes') { if (action !== 'notes') {
new_state += "/" + action; new_state += "/" + action;
...@@ -196,8 +214,13 @@ ...@@ -196,8 +214,13 @@
if (this.diffsLoaded) { if (this.diffsLoaded) {
return; return;
} }
// We extract pathname for the current Changes tab anchor href
// some pages like MergeRequestsController#new has query parameters on that anchor
var url = gl.utils.parseUrl(source);
return this._get({ return this._get({
url: (source + ".json") + this._location.search, url: (url.pathname + ".json") + this._location.search,
success: (function(_this) { success: (function(_this) {
return function(data) { return function(data) {
$('#diffs').html(data.html); $('#diffs').html(data.html);
...@@ -209,7 +232,7 @@ ...@@ -209,7 +232,7 @@
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
$('#diffs .js-syntax-highlight').syntaxHighlight(); $('#diffs .js-syntax-highlight').syntaxHighlight();
$('#diffs .diff-file').singleFileDiff(); $('#diffs .diff-file').singleFileDiff();
if (_this.diffViewType() === 'parallel') { if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) {
_this.expandViewContainer(); _this.expandViewContainer();
} }
_this.diffsLoaded = true; _this.diffsLoaded = true;
...@@ -308,11 +331,25 @@ ...@@ -308,11 +331,25 @@
MergeRequestTabs.prototype.diffViewType = function() { MergeRequestTabs.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type'); return $('.inline-parallel-buttons a.active').data('view-type');
// Returns diff view type };
MergeRequestTabs.prototype.isDiffAction = function(action) {
return action === 'diffs' || action === 'new/diffs'
}; };
MergeRequestTabs.prototype.expandViewContainer = function() { MergeRequestTabs.prototype.expandViewContainer = function() {
return $('.container-fluid').removeClass('container-limited'); var $wrapper = $('.content-wrapper .container-fluid');
if (this.fixedLayoutPref === null) {
this.fixedLayoutPref = $wrapper.hasClass('container-limited');
}
$wrapper.removeClass('container-limited');
};
MergeRequestTabs.prototype.resetViewContainer = function() {
if (this.fixedLayoutPref !== null) {
$('.content-wrapper .container-fluid')
.toggleClass('container-limited', this.fixedLayoutPref);
}
}; };
MergeRequestTabs.prototype.shrinkView = function() { MergeRequestTabs.prototype.shrinkView = function() {
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
this.currentProject = JSON.parse(currentProject); this.currentProject = JSON.parse(currentProject);
} }
$('.js-milestone-select').each(function(i, dropdown) { $('.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; var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove;
$dropdown = $(dropdown); $dropdown = $(dropdown);
projectId = $dropdown.data('project-id'); projectId = $dropdown.data('project-id');
milestonesUrl = $dropdown.data('milestones'); milestonesUrl = $dropdown.data('milestones');
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
selectedMilestone = $dropdown.data('selected'); selectedMilestone = $dropdown.data('selected');
showNo = $dropdown.data('show-no'); showNo = $dropdown.data('show-no');
showAny = $dropdown.data('show-any'); showAny = $dropdown.data('show-any');
showMenuAbove = $dropdown.data('showMenuAbove');
showUpcoming = $dropdown.data('show-upcoming'); showUpcoming = $dropdown.data('show-upcoming');
useId = $dropdown.data('use-id'); useId = $dropdown.data('use-id');
defaultLabel = $dropdown.data('default-label'); defaultLabel = $dropdown.data('default-label');
...@@ -31,12 +32,12 @@ ...@@ -31,12 +32,12 @@
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>'); collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>');
} }
return $dropdown.glDropdown({ return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) { data: function(term, callback) {
return $.ajax({ return $.ajax({
url: milestonesUrl url: milestonesUrl
}).done(function(data) { }).done(function(data) {
var extraOptions; var extraOptions = [];
extraOptions = [];
if (showAny) { if (showAny) {
extraOptions.push({ extraOptions.push({
id: 0, id: 0,
...@@ -58,10 +59,14 @@ ...@@ -58,10 +59,14 @@
title: 'Upcoming' title: 'Upcoming'
}); });
} }
if (extraOptions.length > 2) { if (extraOptions.length) {
extraOptions.push('divider'); extraOptions.push('divider');
} }
return callback(extraOptions.concat(data));
callback(extraOptions.concat(data));
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
}); });
}, },
filterable: true, filterable: true,
...@@ -69,19 +74,20 @@ ...@@ -69,19 +74,20 @@
fields: ['title'] fields: ['title']
}, },
selectable: true, selectable: true,
toggleLabel: function(selected) { toggleLabel: function(selected, el, e) {
if (selected && 'id' in selected) { if (selected && 'id' in selected && $(el).hasClass('is-active')) {
return selected.title; return selected.title;
} else { } else {
return defaultLabel; return defaultLabel;
} }
}, },
defaultLabel: defaultLabel,
fieldName: $dropdown.data('field-name'), fieldName: $dropdown.data('field-name'),
text: function(milestone) { text: function(milestone) {
return _.escape(milestone.title); return _.escape(milestone.title);
}, },
id: function(milestone) { id: function(milestone) {
if (!useId) { if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
return milestone.name; return milestone.name;
} else { } else {
return milestone.id; return milestone.id;
...@@ -100,7 +106,8 @@ ...@@ -100,7 +106,8 @@
page = $('body').data('page'); page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index'); isMRIndex = (page === page && page === 'projects:merge_requests:index');
if ($dropdown.hasClass('js-filter-bulk-update')) { if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
return; return;
} }
if (page === 'projects:boards:show') { if (page === 'projects:boards:show') {
......
...@@ -3,12 +3,21 @@ ...@@ -3,12 +3,21 @@
const $pipelineBtn = $(this).closest('.toggle-pipeline-btn'); const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph'); const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
const $btnText = $(this).find('.toggle-btn-text'); const $btnText = $(this).find('.toggle-btn-text');
const $icon = $(this).find('.fa');
$($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed'); $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed'); const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
const expandIcon = 'fa-caret-down';
const hideIcon = 'fa-caret-up';
graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide') if(graphCollapsed) {
$btnText.text('Expand');
$icon.removeClass(hideIcon).addClass(expandIcon);
} else {
$btnText.text('Hide');
$icon.removeClass(expandIcon).addClass(hideIcon);
}
} }
$(document).on('click', '.toggle-pipeline-btn', toggleGraph); $(document).on('click', '.toggle-pipeline-btn', toggleGraph);
......
(function() { ((global) => {
var GitLabCrop,
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
GitLabCrop = (function() { // Matches everything but the file name
var FILENAMEREGEX; const FILENAMEREGEX = /^.*[\\\/]/;
// Matches everything but the file name class GitLabCrop {
FILENAMEREGEX = /^.*[\\\/]/; constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg,
exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) {
function GitLabCrop(input, opts) { this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this);
var ref, ref1, ref2, ref3, ref4; this.onModalHide = this.onModalHide.bind(this);
if (opts == null) { this.onModalShow = this.onModalShow.bind(this);
opts = {}; this.onPickImageClick = this.onPickImageClick.bind(this);
}
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 = $(input);
// We should rename to avoid spec to fail
// Form will submit the proper input filed with a file using FormData
this.fileInput.attr('name', (this.fileInput.attr('name')) + "-trigger").attr('id', (this.fileInput.attr('id')) + "-trigger");
// Set defaults
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;
// Required params
// Ensure needed elements are jquery objects
// If selector is provided we will convert them to a jQuery Object
this.filename = this.getElement(this.filename);
this.previewImage = this.getElement(this.previewImage);
this.pickImageEl = this.getElement(this.pickImageEl);
// Modal elements usually are outside the @form element
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.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `this.fileInput.attr('id')-trigger`);
this.exportWidth = exportWidth;
this.exportHeight = exportHeight;
this.cropBoxWidth = cropBoxWidth;
this.cropBoxHeight = cropBoxHeight;
this.form = this.fileInput.parents('form');
this.filename = filename;
this.previewImage = previewImage;
this.modalCrop = modalCrop;
this.pickImageEl = pickImageEl;
this.uploadImageBtn = uploadImageBtn;
this.modalCropImg = modalCropImg;
this.filename = this.getElement(filename);
this.previewImage = this.getElement(previewImage);
this.pickImageEl = this.getElement(pickImageEl);
this.modalCrop = _.isString(modalCrop) ? $(modalCrop) : modalCrop;
this.uploadImageBtn = _.isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn;
this.modalCropImg = _.isString(modalCropImg) ? $(modalCropImg) : modalCropImg;
this.cropActionsBtn = this.modalCrop.find('[data-method]'); this.cropActionsBtn = this.modalCrop.find('[data-method]');
this.bindEvents(); this.bindEvents();
} }
GitLabCrop.prototype.getElement = function(selector) { getElement(selector) {
return $(selector, this.form); return $(selector, this.form);
}; }
GitLabCrop.prototype.bindEvents = function() { bindEvents() {
var _this; var _this;
_this = this; _this = this;
this.fileInput.on('change', function(e) { this.fileInput.on('change', function(e) {
...@@ -57,13 +55,13 @@ ...@@ -57,13 +55,13 @@
return _this.onActionBtnClick(btn); return _this.onActionBtnClick(btn);
}); });
return this.croppedImageBlob = null; return this.croppedImageBlob = null;
}; }
GitLabCrop.prototype.onPickImageClick = function() { onPickImageClick() {
return this.fileInput.trigger('click'); return this.fileInput.trigger('click');
}; }
GitLabCrop.prototype.onModalShow = function() { onModalShow() {
var _this; var _this;
_this = this; _this = this;
return this.modalCropImg.cropper({ return this.modalCropImg.cropper({
...@@ -95,44 +93,44 @@ ...@@ -95,44 +93,44 @@
}); });
} }
}); });
}; }
GitLabCrop.prototype.onModalHide = function() { onModalHide() {
return this.modalCropImg.attr('src', '').cropper('destroy'); return this.modalCropImg.attr('src', '').cropper('destroy');
}; }
GitLabCrop.prototype.onUploadImageBtnClick = function(e) { // Remove attached image onUploadImageBtnClick(e) {
e.preventDefault(); // Destroy cropper instance e.preventDefault();
this.setBlob(); this.setBlob();
this.setPreview(); this.setPreview();
this.modalCrop.modal('hide'); this.modalCrop.modal('hide');
return this.fileInput.val(''); return this.fileInput.val('');
}; }
GitLabCrop.prototype.onActionBtnClick = function(btn) { onActionBtnClick(btn) {
var data, result; var data, result;
data = $(btn).data(); data = $(btn).data();
if (this.modalCropImg.data('cropper') && data.method) { if (this.modalCropImg.data('cropper') && data.method) {
return result = this.modalCropImg.cropper(data.method, data.option); return result = this.modalCropImg.cropper(data.method, data.option);
} }
}; }
GitLabCrop.prototype.onFileInputChange = function(e, input) { onFileInputChange(e, input) {
return this.readFile(input); return this.readFile(input);
}; }
GitLabCrop.prototype.readFile = function(input) { readFile(input) {
var _this, reader; var _this, reader;
_this = this; _this = this;
reader = new FileReader; reader = new FileReader;
reader.onload = function() { reader.onload = () => {
_this.modalCropImg.attr('src', reader.result); _this.modalCropImg.attr('src', reader.result);
return _this.modalCrop.modal('show'); return _this.modalCrop.modal('show');
}; };
return reader.readAsDataURL(input.files[0]); return reader.readAsDataURL(input.files[0]);
}; }
GitLabCrop.prototype.dataURLtoBlob = function(dataURL) { dataURLtoBlob(dataURL) {
var array, binary, i, k, len, v; var array, binary, i, k, len, v;
binary = atob(dataURL.split(',')[1]); binary = atob(dataURL.split(',')[1]);
array = []; array = [];
...@@ -143,35 +141,32 @@ ...@@ -143,35 +141,32 @@
return new Blob([new Uint8Array(array)], { return new Blob([new Uint8Array(array)], {
type: 'image/png' type: 'image/png'
}); });
}; }
GitLabCrop.prototype.setPreview = function() { setPreview() {
var filename; var filename;
this.previewImage.attr('src', this.dataURL); this.previewImage.attr('src', this.dataURL);
filename = this.fileInput.val().replace(FILENAMEREGEX, ''); filename = this.fileInput.val().replace(FILENAMEREGEX, '');
return this.filename.text(filename); return this.filename.text(filename);
}; }
GitLabCrop.prototype.setBlob = function() { setBlob() {
this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', { this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', {
width: 200, width: 200,
height: 200 height: 200
}).toDataURL('image/png'); }).toDataURL('image/png');
return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL); return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL);
}; }
GitLabCrop.prototype.getBlob = function() { getBlob() {
return this.croppedImageBlob; return this.croppedImageBlob;
}; }
}
return GitLabCrop;
})();
$.fn.glCrop = function(opts) { $.fn.glCrop = function(opts) {
return this.each(function() { return this.each(function() {
return $(this).data('glcrop', new GitLabCrop(this, opts)); return $(this).data('glcrop', new GitLabCrop(this, opts));
}); });
}; }
}).call(this); })(window.gl || (window.gl = {}));
(function() { ((global) => {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.Profile = (function() { class Profile {
function Profile(opts) { constructor({ form } = {}) {
var cropOpts, ref; this.onSubmitForm = this.onSubmitForm.bind(this);
if (opts == null) { this.form = form || $('.edit-user');
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();
// Automatically submit the Preferences form when any of its radio buttons change
});
$('#user_notification_email').on('change', function() {
return $(this).parents('form').submit();
// Automatically submit email form when it changes
});
$('.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(); this.bindEvents();
cropOpts = { this.initAvatarGlCrop();
}
initAvatarGlCrop() {
const cropOpts = {
filename: '.js-avatar-filename', filename: '.js-avatar-filename',
previewImage: '.avatar-image .avatar', previewImage: '.avatar-image .avatar',
modalCrop: '.modal-profile-crop', modalCrop: '.modal-profile-crop',
...@@ -46,23 +20,51 @@ ...@@ -46,23 +20,51 @@
this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop'); this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
} }
Profile.prototype.bindEvents = function() { bindEvents() {
return this.form.on('submit', this.onSubmitForm); $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
}; $('#user_notification_email').on('change', this.submitForm);
$('.update-username').on('ajax:before', this.beforeUpdateUsername);
$('.update-username').on('ajax:complete', this.afterUpdateUsername);
$('.update-notifications').on('ajax:success', this.onUpdateNotifs);
this.form.on('submit', this.onSubmitForm);
}
submitForm() {
return $(this).parents('form').submit();
}
Profile.prototype.onSubmitForm = function(e) { onSubmitForm(e) {
e.preventDefault(); e.preventDefault();
return this.saveForm(); return this.saveForm();
}; }
beforeUpdateUsername() {
$('.loading-username').show();
$(this).find('.update-success').hide();
return $(this).find('.update-failed').hide();
}
afterUpdateUsername() {
$('.loading-username').hide();
$(this).find('.btn-save').enable();
return $(this).find('.loading-gif').hide();
}
onUpdateNotifs(e, data) {
return data.saved ?
new Flash("Notification settings saved", "notice") :
new Flash("Failed to save new settings", "alert");
}
saveForm() {
const self = this;
const formData = new FormData(this.form[0]);
const avatarBlob = this.avatarGlCrop.getBlob();
Profile.prototype.saveForm = function() {
var avatarBlob, formData, self;
self = this;
formData = new FormData(this.form[0]);
avatarBlob = this.avatarGlCrop.getBlob();
if (avatarBlob != null) { if (avatarBlob != null) {
formData.append('user[avatar]', avatarBlob, 'avatar.png'); formData.append('user[avatar]', avatarBlob, 'avatar.png');
} }
return $.ajax({ return $.ajax({
url: this.form.attr('action'), url: this.form.attr('action'),
type: this.form.attr('method'), type: this.form.attr('method'),
...@@ -70,37 +72,29 @@ ...@@ -70,37 +72,29 @@
dataType: "json", dataType: "json",
processData: false, processData: false,
contentType: false, contentType: false,
success: function(response) { success: response => new Flash(response.message, 'notice'),
return new Flash(response.message, 'notice'); error: jqXHR => new Flash(jqXHR.responseJSON.message, 'alert'),
}, complete: () => {
error: function(jqXHR) {
return new Flash(jqXHR.responseJSON.message, 'alert');
},
complete: function() {
window.scrollTo(0, 0); window.scrollTo(0, 0);
// Enable submit button after requests ends // Enable submit button after requests ends
return self.form.find(':input[disabled]').enable(); return self.form.find(':input[disabled]').enable();
} }
}); });
}; }
}
return Profile;
})();
$(function() { $(function() {
$(document).on('focusout.ssh_key', '#key_key', function() { $(document).on('focusout.ssh_key', '#key_key', function() {
var $title, comment; const $title = $('#key_title');
$title = $('#key_title'); const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
if (comment && comment.length > 1 && $title.val() === '') { if (comment && comment.length > 1 && $title.val() === '') {
return $title.val(comment[1]).change(); return $title.val(comment[1]).change();
} }
// Extract the SSH Key title from its comment // Extract the SSH Key title from its comment
}); });
if (gl.utils.getPagePath() === 'profiles') { if (global.utils.getPagePath() === 'profiles') {
return new Profile(); return new Profile();
} }
}); });
}).call(this); })(window.gl || (window.gl = {}));
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
data = groups.concat(projects); data = groups.concat(projects);
return finalCallback(data); return finalCallback(data);
}; };
return Api.groups(term, false, groupsCallback); return Api.groups(term, false, false, groupsCallback);
}; };
} else { } else {
projectsCallback = finalCallback; projectsCallback = finalCallback;
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
data = groups.concat(projects); data = groups.concat(projects);
return finalCallback(data); return finalCallback(data);
}; };
return Api.groups(query.term, false, groupsCallback); return Api.groups(query.term, false, false, groupsCallback);
}; };
} else { } else {
projectsCallback = finalCallback; projectsCallback = finalCallback;
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
filterable: true, filterable: true,
fieldName: 'group_id', fieldName: 'group_id',
data: function(term, callback) { data: function(term, callback) {
return Api.groups(term, null, function(data) { return Api.groups(term, false, false, function(data) {
data.unshift({ data.unshift({
name: 'Any' name: 'Any'
}); });
......
/*= require ../blob/template_selector */ /*= require ../blob/template_selector */
((global) => { ((global) => {
class IssuableTemplateSelector extends TemplateSelector { class IssuableTemplateSelector extends gl.TemplateSelector {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
this.projectPath = this.dropdown.data('project-path'); this.projectPath = this.dropdown.data('project-path');
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
if (initialQuery.name) this.requestFile(initialQuery); if (initialQuery.name) this.requestFile(initialQuery);
$('.reset-template', this.dropdown.parent()).on('click', () => { $('.reset-template', this.dropdown.parent()).on('click', () => {
if (this.currentTemplate) this.setInputValueToTemplateContent(); if (this.currentTemplate) this.setInputValueToTemplateContent(false);
}); });
} }
...@@ -26,26 +26,28 @@ ...@@ -26,26 +26,28 @@
this.currentTemplate = currentTemplate; this.currentTemplate = currentTemplate;
if (err) return; // Error handled by global AJAX error handler if (err) return; // Error handled by global AJAX error handler
this.stopLoadingSpinner(); this.stopLoadingSpinner();
this.setInputValueToTemplateContent(); this.setInputValueToTemplateContent(true);
}); });
return; return;
} }
setInputValueToTemplateContent() { setInputValueToTemplateContent(append) {
// `this.requestFileSuccess` sets the value of the description input field // `this.requestFileSuccess` sets the value of the description input field
// to the content of the template selected. // to the content of the template selected. If `append` is true, the
// template content will be appended to the previous value of the field,
// separated by a blank line if the previous value is non-empty.
if (this.titleInput.val() === '') { if (this.titleInput.val() === '') {
// If the title has not yet been set, focus the title input and // If the title has not yet been set, focus the title input and
// skip focusing the description input by setting `true` as the 2nd // skip focusing the description input by setting `true` as the
// argument to `requestFileSuccess`. // `skipFocus` option to `requestFileSuccess`.
this.requestFileSuccess(this.currentTemplate, true); this.requestFileSuccess(this.currentTemplate, {skipFocus: true, append});
this.titleInput.focus(); this.titleInput.focus();
} else { } else {
this.requestFileSuccess(this.currentTemplate); this.requestFileSuccess(this.currentTemplate, {skipFocus: false, append});
} }
return; return;
} }
} }
global.IssuableTemplateSelector = IssuableTemplateSelector; global.IssuableTemplateSelector = IssuableTemplateSelector;
})(window); })(window.gl || (window.gl = {}));
((global) => { ((global) => {
class IssuableTemplateSelectors { class IssuableTemplateSelectors {
constructor(opts = {}) { constructor({ $dropdowns, editor } = {}) {
this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector'); this.$dropdowns = $dropdowns || $('.js-issuable-selector');
this.editor = opts.editor || this.initEditor(); this.editor = editor || this.initEditor();
this.$dropdowns.each((i, dropdown) => { this.$dropdowns.each((i, dropdown) => {
let $dropdown = $(dropdown); const $dropdown = $(dropdown);
new IssuableTemplateSelector({ new gl.IssuableTemplateSelector({
pattern: /(\.md)/, pattern: /(\.md)/,
data: $dropdown.data('data'), data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-issuable-selector-wrap'), wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
...@@ -26,4 +26,4 @@ ...@@ -26,4 +26,4 @@
} }
global.IssuableTemplateSelectors = IssuableTemplateSelectors; global.IssuableTemplateSelectors = IssuableTemplateSelectors;
})(window); })(window.gl || (window.gl = {}));
(function() { ((global) => {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
class Todos {
this.Todos = (function() { constructor({ el } = {}) {
function Todos(opts) { this.allDoneClicked = this.allDoneClicked.bind(this);
var ref; this.doneClicked = this.doneClicked.bind(this);
if (opts == null) { this.el = el || $('.js-todos-options');
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.perPage = this.el.data('perPage');
this.clearListeners(); this.clearListeners();
this.initBtnListeners(); this.initBtnListeners();
this.initFilters(); this.initFilters();
} }
Todos.prototype.clearListeners = function() { clearListeners() {
$('.done-todo').off('click'); $('.done-todo').off('click');
$('.js-todos-mark-all').off('click'); $('.js-todos-mark-all').off('click');
return $('.todo').off('click'); return $('.todo').off('click');
}; }
Todos.prototype.initBtnListeners = function() { initBtnListeners() {
$('.done-todo').on('click', this.doneClicked); $('.done-todo').on('click', this.doneClicked);
$('.js-todos-mark-all').on('click', this.allDoneClicked); $('.js-todos-mark-all').on('click', this.allDoneClicked);
return $('.todo').on('click', this.goToTodoUrl); return $('.todo').on('click', this.goToTodoUrl);
}; }
Todos.prototype.initFilters = function() { initFilters() {
new UsersSelect(); new UsersSelect();
this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']); this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
this.initFilterDropdown($('.js-type-search'), 'type'); this.initFilterDropdown($('.js-type-search'), 'type');
...@@ -38,125 +33,117 @@ ...@@ -38,125 +33,117 @@
event.preventDefault(); event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize()); Turbolinks.visit(this.action + '&' + $(this).serialize());
}); });
}; }
Todos.prototype.initFilterDropdown = function($dropdown, fieldName, searchFields) { initFilterDropdown($dropdown, fieldName, searchFields) {
$dropdown.glDropdown({ $dropdown.glDropdown({
fieldName,
selectable: true, selectable: true,
filterable: searchFields ? true : false, filterable: searchFields ? true : false,
fieldName: fieldName,
search: { fields: searchFields }, search: { fields: searchFields },
data: $dropdown.data('data'), data: $dropdown.data('data'),
clicked: function() { clicked: function() {
return $dropdown.closest('form.filter-form').submit(); return $dropdown.closest('form.filter-form').submit();
} }
}) })
}; }
Todos.prototype.doneClicked = function(e) { doneClicked(e) {
var $this;
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
$this = $(e.currentTarget); const $target = $(e.currentTarget);
$this.disable(); $target.disable();
return $.ajax({ return $.ajax({
type: 'POST', type: 'POST',
url: $this.attr('href'), url: $target.attr('href'),
dataType: 'json', dataType: 'json',
data: { data: {
'_method': 'delete' '_method': 'delete'
}, },
success: (function(_this) { success: (data) => {
return function(data) { this.redirectIfNeeded(data.count);
_this.redirectIfNeeded(data.count); this.clearDone($target.closest('li'));
_this.clearDone($this.closest('li')); return this.updateBadges(data);
return _this.updateBadges(data); }
};
})(this)
}); });
}; }
Todos.prototype.allDoneClicked = function(e) { allDoneClicked(e) {
var $this;
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
$this = $(e.currentTarget); $target = $(e.currentTarget);
$this.disable(); $target.disable();
return $.ajax({ return $.ajax({
type: 'POST', type: 'POST',
url: $this.attr('href'), url: $target.attr('href'),
dataType: 'json', dataType: 'json',
data: { data: {
'_method': 'delete' '_method': 'delete'
}, },
success: (function(_this) { success: (data) => {
return function(data) { $target.remove();
$this.remove(); $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>');
$('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>'); return this.updateBadges(data);
return _this.updateBadges(data); }
};
})(this)
}); });
}; }
Todos.prototype.clearDone = function($row) { clearDone($row) {
var $ul; const $ul = $row.closest('ul');
$ul = $row.closest('ul');
$row.remove(); $row.remove();
if (!$ul.find('li').length) { if (!$ul.find('li').length) {
return $ul.parents('.panel').remove(); return $ul.parents('.panel').remove();
} }
}; }
Todos.prototype.updateBadges = function(data) { updateBadges(data) {
$('.todos-pending .badge, .todos-pending-count').text(data.count); $('.todos-pending .badge, .todos-pending-count').text(data.count);
return $('.todos-done .badge').text(data.done_count); return $('.todos-done .badge').text(data.done_count);
}; }
Todos.prototype.getTotalPages = function() { getTotalPages() {
return this.el.data('totalPages'); return this.el.data('totalPages');
}; }
Todos.prototype.getCurrentPage = function() { getCurrentPage() {
return this.el.data('currentPage'); return this.el.data('currentPage');
}; }
Todos.prototype.getTodosPerPage = function() { getTodosPerPage() {
return this.el.data('perPage'); return this.el.data('perPage');
}; }
redirectIfNeeded(total) {
const currPages = this.getTotalPages();
const currPage = this.getCurrentPage();
Todos.prototype.redirectIfNeeded = function(total) {
var currPage, currPages, newPages, pageParams, url;
currPages = this.getTotalPages();
currPage = this.getCurrentPage();
// Refresh if no remaining Todos // Refresh if no remaining Todos
if (!total) { if (!total) {
location.reload(); window.location.reload();
return; return;
} }
// Do nothing if no pagination // Do nothing if no pagination
if (!currPages) { if (!currPages) {
return; return;
} }
newPages = Math.ceil(total / this.getTodosPerPage());
// Includes query strings const newPages = Math.ceil(total / this.getTodosPerPage());
url = location.href; let url = location.href;
// If new total of pages is different than we have now
if (newPages !== currPages) { if (newPages !== currPages) {
// Redirect to previous page if there's one available // Redirect to previous page if there's one available
if (currPages > 1 && currPage === currPages) { if (currPages > 1 && currPage === currPages) {
pageParams = { const pageParams = {
page: currPages - 1 page: currPages - 1
}; };
url = gl.utils.mergeUrlParams(pageParams, url); url = gl.utils.mergeUrlParams(pageParams, url);
} }
return Turbolinks.visit(url); return Turbolinks.visit(url);
} }
}; }
Todos.prototype.goToTodoUrl = function(e) { goToTodoUrl(e) {
var todoLink; const todoLink = $(this).data('url');
todoLink = $(this).data('url');
if (!todoLink) { if (!todoLink) {
return; return;
} }
...@@ -167,10 +154,8 @@ ...@@ -167,10 +154,8 @@
} else { } else {
return Turbolinks.visit(todoLink); return Turbolinks.visit(todoLink);
} }
}; }
}
return Todos;
})();
}).call(this); global.Todos = Todos;
})(window.gl || (window.gl = {}));
(global => { ((global) => {
global.User = class { global.User = class {
constructor(opts) { constructor({ action }) {
this.opts = opts; this.action = action;
this.placeProfileAvatarsToTop(); this.placeProfileAvatarsToTop();
this.initTabs(); this.initTabs();
this.hideProjectLimitMessage(); this.hideProjectLimitMessage();
...@@ -14,9 +14,9 @@ ...@@ -14,9 +14,9 @@
} }
initTabs() { initTabs() {
return new UserTabs({ return new global.UserTabs({
parentEl: '.user-profile', parentEl: '.user-profile',
action: this.opts.action action: this.action
}); });
} }
......
// 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>
//
(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);
// Make jQuery object if selector is provided
if (typeof this.parentEl === 'string') {
this.parentEl = $(this.parentEl);
}
// Store the `location` object, allowing for easier stubbing in tests
this._location = location;
// Set tab states
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;
}
// Actions
this.actions = Object.keys(this.loaded);
this.bindEvents();
// Set active tab
if (this.action === 'show') {
this.action = this.defaultAction;
}
this.activateTab(this.action);
}
UserTabs.prototype.bindEvents = function() {
// Toggle event listeners
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;
// Fix tooltips
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;
// Remove possible actions from URL
regExp = new RegExp('\/(' + this.actions.join('|') + ')(\.html)?\/?$');
new_state = this._location.pathname;
// remove trailing slashes
new_state = new_state.replace(/\/+$/, "");
new_state = new_state.replace(regExp, '');
// Append the new action if we're on a tab other than 'activity'
if (action !== this.defaultAction) {
new_state += "/" + action;
}
// Ensure parameters and hash come along for the ride
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);
/*
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>
*/
((global) => {
class UserTabs {
constructor ({ defaultAction, action, parentEl }) {
this.loaded = {};
this.defaultAction = defaultAction || 'activity';
this.action = action || this.defaultAction;
this.$parentEl = $(parentEl) || $(document);
this._location = window.location;
this.$parentEl.find('.nav-links a')
.each((i, navLink) => {
this.loaded[$(navLink).attr('data-action')] = false;
});
this.actions = Object.keys(this.loaded);
this.bindEvents();
if (this.action === 'show') {
this.action = this.defaultAction;
}
this.activateTab(this.action);
}
bindEvents() {
return this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
.on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event));
}
tabShown(event) {
const $target = $(event.target);
const action = $target.data('action');
const source = $target.attr('href');
this.setTab(source, action);
return this.setCurrentAction(source, action);
}
activateTab(action) {
return this.$parentEl.find(`.nav-links .js-${action}-tab a`)
.tab('show');
}
setTab(source, action) {
if (this.loaded[action]) {
return;
}
if (action === 'activity') {
this.loadActivities(source);
}
const loadableActions = [ 'groups', 'contributed', 'projects', 'snippets' ];
if (loadableActions.indexOf(action) > -1) {
return this.loadTab(source, action);
}
}
loadTab(source, action) {
return $.ajax({
beforeSend: () => this.toggleLoading(true),
complete: () => this.toggleLoading(false),
dataType: 'json',
type: 'GET',
url: `${source}.json`,
success: (data) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
this.loaded[action] = true;
return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
}
});
}
loadActivities(source) {
if (this.loaded['activity']) {
return;
}
const $calendarWrap = this.$parentEl.find('.user-calendar');
$calendarWrap.load($calendarWrap.data('href'));
new Activities();
return this.loaded['activity'] = true;
}
toggleLoading(status) {
return this.$parentEl.find('.loading-status .loading')
.toggle(status);
}
setCurrentAction(source, action) {
let new_state = source
new_state = new_state.replace(/\/+$/, '');
new_state += this._location.search + this._location.hash;
history.replaceState({
turbolinks: true,
url: new_state
}, document.title, new_state);
return new_state;
}
}
global.UserTabs = UserTabs;
})(window.gl || (window.gl = {}));
...@@ -14,11 +14,12 @@ ...@@ -14,11 +14,12 @@
$('.js-user-search').each((function(_this) { $('.js-user-search').each((function(_this) {
return function(i, dropdown) { return function(i, dropdown) {
var options = {}; var options = {};
var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser; var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove;
$dropdown = $(dropdown); $dropdown = $(dropdown);
options.projectId = $dropdown.data('project-id'); options.projectId = $dropdown.data('project-id');
options.showCurrentUser = $dropdown.data('current-user'); options.showCurrentUser = $dropdown.data('current-user');
showNullUser = $dropdown.data('null-user'); showNullUser = $dropdown.data('null-user');
showMenuAbove = $dropdown.data('showMenuAbove');
showAnyUser = $dropdown.data('any-user'); showAnyUser = $dropdown.data('any-user');
firstUser = $dropdown.data('first-user'); firstUser = $dropdown.data('first-user');
options.authorId = $dropdown.data('author-id'); options.authorId = $dropdown.data('author-id');
...@@ -70,9 +71,10 @@ ...@@ -70,9 +71,10 @@
return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); 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> <% } %>'); collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/<%- 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> <% } %>'); assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/<%- 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({ return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) { data: function(term, callback) {
var isAuthorFilter; var isAuthorFilter;
isAuthorFilter = $('.js-author-search'); isAuthorFilter = $('.js-author-search');
...@@ -116,8 +118,11 @@ ...@@ -116,8 +118,11 @@
if (showDivider) { if (showDivider) {
users.splice(showDivider, 0, "divider"); users.splice(showDivider, 0, "divider");
} }
// Send the data back
return callback(users); callback(users);
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
}); });
}, },
filterable: true, filterable: true,
...@@ -127,8 +132,8 @@ ...@@ -127,8 +132,8 @@
}, },
selectable: true, selectable: true,
fieldName: $dropdown.data('field-name'), fieldName: $dropdown.data('field-name'),
toggleLabel: function(selected) { toggleLabel: function(selected, el) {
if (selected && 'id' in selected) { if (selected && 'id' in selected && $(el).hasClass('is-active')) {
if (selected.text) { if (selected.text) {
return selected.text; return selected.text;
} else { } else {
...@@ -138,6 +143,7 @@ ...@@ -138,6 +143,7 @@
return defaultLabel; return defaultLabel;
} }
}, },
defaultLabel: defaultLabel,
inputId: 'issue_assignee_id', inputId: 'issue_assignee_id',
hidden: function(e) { hidden: function(e) {
$selectbox.hide(); $selectbox.hide();
...@@ -149,7 +155,9 @@ ...@@ -149,7 +155,9 @@
page = $('body').data('page'); page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index'); isMRIndex = (page === page && page === 'projects:merge_requests:index');
if ($dropdown.hasClass('js-filter-bulk-update')) { if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
selectedId = user.id;
return; return;
} }
if (page === 'projects:boards:show') { if (page === 'projects:boards:show') {
...@@ -167,6 +175,9 @@ ...@@ -167,6 +175,9 @@
return assignTo(selected); return assignTo(selected);
} }
}, },
id: function (user) {
return user.id;
},
renderRow: function(user) { renderRow: function(user) {
var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username; var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username;
username = user.username ? "@" + user.username : ""; username = user.username ? "@" + user.username : "";
......
...@@ -194,10 +194,17 @@ ...@@ -194,10 +194,17 @@
pointer-events: none !important; pointer-events: none !important;
} }
.caret { .fa-caret-down,
.fa-caret-up {
margin-left: 5px; margin-left: 5px;
} }
&.dropdown-toggle {
.fa-caret-down {
margin-left: 3px;
}
}
svg { svg {
height: 15px; height: 15px;
width: 15px; width: 15px;
......
.caret {
display: inline-block;
width: 0;
height: 0;
margin-left: 2px;
vertical-align: middle;
border-top: $caret-width-base dashed;
border-right: $caret-width-base solid transparent;
border-left: $caret-width-base solid transparent;
}
.btn-group {
.caret {
margin-left: 0;
}
}
.dropdown { .dropdown {
position: relative; position: relative;
...@@ -604,3 +587,9 @@ ...@@ -604,3 +587,9 @@
display: block; display: block;
color: $gl-placeholder-color; color: $gl-placeholder-color;
} }
.dropdown-toggle-text {
&.is-default {
color: $gl-placeholder-color;
}
}
...@@ -21,7 +21,8 @@ ...@@ -21,7 +21,8 @@
.flash-notice, .flash-alert { .flash-notice, .flash-alert {
border-radius: $border-radius-default; border-radius: $border-radius-default;
.container-fluid.container-limited.flash-text { .container-fluid,
.container-fluid.container-limited {
background: transparent; background: transparent;
} }
} }
...@@ -35,12 +36,6 @@ ...@@ -35,12 +36,6 @@
} }
} }
.content-wrapper {
.flash-notice .container-fluid {
background-color: transparent;
}
}
@media (max-width: $screen-md-min) { @media (max-width: $screen-md-min) {
ul.notes { ul.notes {
.flash-container.timeline-content { .flash-container.timeline-content {
......
...@@ -81,10 +81,10 @@ label { ...@@ -81,10 +81,10 @@ label {
.select-wrapper { .select-wrapper {
position: relative; position: relative;
.caret { .fa-caret-down {
position: absolute; position: absolute;
right: 10px; right: 10px;
top: $gl-padding; top: 10px;
color: $gray-darkest; color: $gray-darkest;
pointer-events: none; pointer-events: none;
} }
......
...@@ -57,6 +57,10 @@ header { ...@@ -57,6 +57,10 @@ header {
&:hover, &:focus, &:active { &:hover, &:focus, &:active {
background-color: $background-color; background-color: $background-color;
} }
.fa-caret-down {
font-size: 15px;
}
} }
.navbar-toggle { .navbar-toggle {
......
...@@ -21,7 +21,14 @@ ...@@ -21,7 +21,14 @@
padding-right: 10px; padding-right: 10px;
b { b {
@extend .caret; display: inline-block;
width: 0;
height: 0;
margin-left: 2px;
vertical-align: middle;
border-top: $caret-width-base dashed;
border-right: $caret-width-base solid transparent;
border-left: $caret-width-base solid transparent;
color: $gray-darkest; color: $gray-darkest;
} }
} }
......
...@@ -162,6 +162,10 @@ lex ...@@ -162,6 +162,10 @@ lex
list-style: none; list-style: none;
overflow-y: scroll; overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
&.is-smaller {
height: calc(100% - 185px);
}
} }
.board-list-loading { .board-list-loading {
...@@ -233,3 +237,31 @@ lex ...@@ -233,3 +237,31 @@ lex
margin-right: 5px; margin-right: 5px;
} }
} }
.board-new-issue-form {
margin: 5px;
}
.board-issue-count-holder {
margin-top: -3px;
.btn {
line-height: 12px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
.board-issue-count {
padding-right: 10px;
padding-left: 10px;
line-height: 21px;
border-radius: $border-radius-base;
border: 1px solid $border-color;
&.has-btn {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-width: 1px 0 1px 1px;
}
}
.environments-container,
.deployments-container {
width: 100%;
overflow: auto;
}
.environments { .environments {
.deployment-column {
.avatar {
float: none;
}
}
.commit-title { .commit-title {
margin: 0; margin: 0;
...@@ -9,6 +20,7 @@ ...@@ -9,6 +20,7 @@
width: 12px; width: 12px;
} }
.external-url,
.dropdown-new { .dropdown-new {
color: $table-text-gray; color: $table-text-gray;
} }
...@@ -21,16 +33,35 @@ ...@@ -21,16 +33,35 @@
} }
} }
.build-link,
.branch-name { .branch-name {
color: $gl-dark-link-color; color: $gl-dark-link-color;
} }
.deployment {
.build-column {
.build-link {
color: $gl-dark-link-color;
}
.avatar {
float: none;
}
}
}
} }
.table.builds.environments { .table.builds.environments {
min-width: 500px;
.icon-container { .icon-container {
width: 20px; width: 20px;
text-align: center; text-align: center;
} }
.branch-commit {
.commit-id {
margin-right: 0;
}
}
} }
...@@ -59,6 +59,13 @@ ...@@ -59,6 +59,13 @@
width: 200px; width: 200px;
margin-bottom: 0; margin-bottom: 0;
} }
.label {
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
max-width: 100%;
}
} }
.label-description { .label-description {
......
...@@ -70,7 +70,8 @@ ...@@ -70,7 +70,8 @@
&.ci-success { &.ci-success {
color: $gl-success; color: $gl-success;
a.environment { a.environment,
a.pipeline {
color: inherit; color: inherit;
} }
} }
...@@ -349,6 +350,10 @@ ...@@ -349,6 +350,10 @@
.issuable-form-select-holder { .issuable-form-select-holder {
display: inline-block; display: inline-block;
width: 250px; width: 250px;
.dropdown-menu-toggle {
width: 100%;
}
} }
.table-holder { .table-holder {
......
...@@ -22,6 +22,11 @@ ...@@ -22,6 +22,11 @@
.table.builds { .table.builds {
min-width: 1200px; min-width: 1200px;
.branch-commit {
width: 33%;
}
} }
} }
...@@ -224,9 +229,12 @@ ...@@ -224,9 +229,12 @@
.fa { .fa {
color: $table-text-gray; color: $table-text-gray;
margin-right: 6px;
font-size: 14px; font-size: 14px;
} }
svg, .fa {
margin-right: 0;
}
} }
.btn-remove { .btn-remove {
...@@ -267,18 +275,8 @@ ...@@ -267,18 +275,8 @@
.toggle-pipeline-btn { .toggle-pipeline-btn {
background-color: $gray-dark; background-color: $gray-dark;
.caret {
border-top: none;
border-bottom: 4px solid;
}
&.graph-collapsed { &.graph-collapsed {
background-color: $white-light; background-color: $white-light;
.caret {
border-bottom: none;
border-top: 4px solid;
}
} }
} }
...@@ -385,6 +383,8 @@ ...@@ -385,6 +383,8 @@
left: auto; left: auto;
right: -214px; right: -214px;
top: -9px; top: -9px;
max-height: 245px;
overflow-y: scroll;
a:hover { a:hover {
.ci-status-text { .ci-status-text {
......
...@@ -94,7 +94,7 @@ ...@@ -94,7 +94,7 @@
.profile-user-bio { .profile-user-bio {
// Limits the width of the user bio for readability. // Limits the width of the user bio for readability.
max-width: 600px; max-width: 600px;
margin: 15px auto 0; margin: 10px auto;
padding: 0 16px; padding: 0 16px;
} }
...@@ -213,29 +213,22 @@ ...@@ -213,29 +213,22 @@
} }
.user-profile { .user-profile {
.cover-controls a { .cover-controls a {
margin-left: 5px; margin-left: 5px;
} }
.profile-header { .profile-header {
margin: 0 auto; margin: 0 auto;
.avatar-holder { .avatar-holder {
width: 90px; width: 90px;
display: inline-block; margin: 0 auto 10px;
}
.user-info {
display: inline-block;
text-align: left;
vertical-align: middle;
margin-left: 15px;
.handle {
color: $gl-gray-light;
}
.member-date {
margin-bottom: 4px;
}
} }
} }
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.cover-block { .cover-block {
padding-top: 20px; padding-top: 20px;
} }
...@@ -258,10 +251,6 @@ ...@@ -258,10 +251,6 @@
} }
} }
.user-profile-nav {
margin-top: 15px;
}
table.u2f-registrations { table.u2f-registrations {
th:not(:last-child), td:not(:last-child) { th:not(:last-child), td:not(:last-child) {
border-right: solid 1px transparent; border-right: solid 1px transparent;
......
...@@ -146,7 +146,8 @@ ...@@ -146,7 +146,8 @@
} }
.project-repo-btn-group, .project-repo-btn-group,
.notification-dropdown { .notification-dropdown,
.project-dropdown {
margin-left: 10px; margin-left: 10px;
} }
......
...@@ -51,6 +51,7 @@ ...@@ -51,6 +51,7 @@
-webkit-flex-direction: column; -webkit-flex-direction: column;
flex-direction: column; flex-direction: column;
margin-left: 10px; margin-left: 10px;
min-width: 55px;
} }
.todo-item { .todo-item {
...@@ -120,6 +121,14 @@ ...@@ -120,6 +121,14 @@
} }
} }
@media (max-width: $screen-sm-max) {
.todos-filters {
.dropdown-menu-toggle {
width: 135px;
}
}
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.todo { .todo {
.avatar { .avatar {
...@@ -141,4 +150,14 @@ ...@@ -141,4 +150,14 @@
padding-left: 10px; padding-left: 10px;
} }
} }
.todos-filters {
.row-content-block {
padding-bottom: 50px;
}
.dropdown-menu-toggle {
width: 100%;
}
}
} }
...@@ -37,7 +37,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController ...@@ -37,7 +37,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
end end
def preview def preview
@message = broadcast_message_params[:message] @broadcast_message = BroadcastMessage.new(broadcast_message_params)
end end
protected protected
......
...@@ -173,7 +173,8 @@ class ApplicationController < ActionController::Base ...@@ -173,7 +173,8 @@ class ApplicationController < ActionController::Base
end end
def event_filter def event_filter
filters = cookies['event_filter'].split(',') if cookies['event_filter'].present? # Split using comma to maintain backward compatibility Ex/ "filter1,filter2"
filters = cookies['event_filter'].split(',')[0] if cookies['event_filter'].present?
@event_filter ||= EventFilter.new(filters) @event_filter ||= EventFilter.new(filters)
end end
......
...@@ -23,15 +23,24 @@ module AuthenticatesWithTwoFactor ...@@ -23,15 +23,24 @@ module AuthenticatesWithTwoFactor
# #
# Returns nil # Returns nil
def prompt_for_two_factor(user) def prompt_for_two_factor(user)
return locked_user_redirect(user) if user.access_locked?
session[:otp_user_id] = user.id session[:otp_user_id] = user.id
setup_u2f_authentication(user) setup_u2f_authentication(user)
render 'devise/sessions/two_factor' render 'devise/sessions/two_factor'
end end
def locked_user_redirect(user)
flash.now[:alert] = 'Invalid Login or password'
render 'devise/sessions/new'
end
def authenticate_with_two_factor def authenticate_with_two_factor
user = self.resource = find_user user = self.resource = find_user
if user_params[:otp_attempt].present? && session[:otp_user_id] if user.access_locked?
locked_user_redirect(user)
elsif user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user) authenticate_with_two_factor_via_otp(user)
elsif user_params[:device_response].present? && session[:otp_user_id] elsif user_params[:device_response].present? && session[:otp_user_id]
authenticate_with_two_factor_via_u2f(user) authenticate_with_two_factor_via_u2f(user)
...@@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor ...@@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1' remember_me(user) if user_params[:remember_me] == '1'
sign_in(user) sign_in(user)
else else
user.increment_failed_attempts!
flash.now[:alert] = 'Invalid two-factor code.' flash.now[:alert] = 'Invalid two-factor code.'
render :two_factor prompt_for_two_factor(user)
end end
end end
...@@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor ...@@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1' remember_me(user) if user_params[:remember_me] == '1'
sign_in(user) sign_in(user)
else else
user.increment_failed_attempts!
flash.now[:alert] = 'Authentication via U2F device failed.' flash.now[:alert] = 'Authentication via U2F device failed.'
prompt_for_two_factor(user) prompt_for_two_factor(user)
end end
......
...@@ -15,18 +15,17 @@ module MembershipActions ...@@ -15,18 +15,17 @@ module MembershipActions
end end
def leave def leave
@member = membershipable.members.find_by(user_id: current_user) || member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id).
membershipable.requesters.find_by(user_id: current_user) execute(:all)
Members::DestroyService.new(@member, current_user).execute
source_type = @member.real_source_type.humanize(capitalize: false) source_type = membershipable.class.to_s.humanize(capitalize: false)
notice = notice =
if @member.request? if member.request?
"Your access request to the #{source_type} has been withdrawn." "Your access request to the #{source_type} has been withdrawn."
else else
"You left the \"#{@member.source.human_name}\" #{source_type}." "You left the \"#{membershipable.human_name}\" #{source_type}."
end end
redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize] redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
redirect_to redirect_path, notice: notice redirect_to redirect_path, notice: notice
end end
......
...@@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end end
def trending def trending
@projects = TrendingProjectsFinder.new.execute(current_user) @projects = TrendingProjectsFinder.new.execute
@projects = filter_projects(@projects) @projects = filter_projects(@projects)
@projects = @projects.page(params[:page]) @projects = @projects.page(params[:page])
......
...@@ -40,10 +40,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -40,10 +40,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end end
def destroy def destroy
@group_member = @group.members.find_by(id: params[:id]) || Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all)
@group.requesters.find_by(id: params[:id])
Members::DestroyService.new(@group_member, current_user).execute
respond_to do |format| respond_to do |format|
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
......
...@@ -2,6 +2,7 @@ module Projects ...@@ -2,6 +2,7 @@ module Projects
module Boards module Boards
class IssuesController < Boards::ApplicationController class IssuesController < Boards::ApplicationController
before_action :authorize_read_issue!, only: [:index] before_action :authorize_read_issue!, only: [:index]
before_action :authorize_create_issue!, only: [:create]
before_action :authorize_update_issue!, only: [:update] before_action :authorize_update_issue!, only: [:update]
def index def index
...@@ -9,16 +10,23 @@ module Projects ...@@ -9,16 +10,23 @@ module Projects
issues = issues.page(params[:page]) issues = issues.page(params[:page])
render json: { render json: {
issues: issues.as_json( issues: serialize_as_json(issues),
only: [:iid, :title, :confidential],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
}),
size: issues.total_count size: issues.total_count
} }
end end
def create
list = project.board.lists.find(params[:list_id])
service = ::Boards::Issues::CreateService.new(project, current_user, issue_params)
issue = service.execute(list)
if issue.valid?
render json: serialize_as_json(issue)
else
render json: issue.errors, status: :unprocessable_entity
end
end
def update def update
service = ::Boards::Issues::MoveService.new(project, current_user, move_params) service = ::Boards::Issues::MoveService.new(project, current_user, move_params)
...@@ -43,6 +51,10 @@ module Projects ...@@ -43,6 +51,10 @@ module Projects
return render_403 unless can?(current_user, :read_issue, project) return render_403 unless can?(current_user, :read_issue, project)
end end
def authorize_create_issue!
return render_403 unless can?(current_user, :admin_issue, project)
end
def authorize_update_issue! def authorize_update_issue!
return render_403 unless can?(current_user, :update_issue, issue) return render_403 unless can?(current_user, :update_issue, issue)
end end
...@@ -54,6 +66,19 @@ module Projects ...@@ -54,6 +66,19 @@ module Projects
def move_params def move_params
params.permit(:id, :from_list_id, :to_list_id) params.permit(:id, :from_list_id, :to_list_id)
end end
def issue_params
params.require(:issue).permit(:title).merge(request: request)
end
def serialize_as_json(resource)
resource.as_json(
only: [:iid, :title, :confidential],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
})
end
end end
end end
end end
class Projects::BoardsController < Projects::ApplicationController class Projects::BoardsController < Projects::ApplicationController
include IssuableCollections
respond_to :html respond_to :html
before_action :authorize_read_board!, only: [:show] before_action :authorize_read_board!, only: [:show]
......
...@@ -4,17 +4,25 @@ class Projects::GroupLinksController < Projects::ApplicationController ...@@ -4,17 +4,25 @@ class Projects::GroupLinksController < Projects::ApplicationController
def index def index
@group_links = project.project_group_links.all @group_links = project.project_group_links.all
@skip_groups = @group_links.pluck(:group_id)
@skip_groups << project.group.try(:id)
end end
def create def create
group = Group.find(params[:link_group_id]) group = Group.find(params[:link_group_id]) if params[:link_group_id].present?
return render_404 unless can?(current_user, :read_group, group)
if group
project.project_group_links.create( return render_404 unless can?(current_user, :read_group, group)
group: group,
group_access: params[:link_group_access], project.project_group_links.create(
expires_at: params[:expires_at] group: group,
) group_access: params[:link_group_access],
expires_at: params[:expires_at]
)
else
flash[:alert] = 'Please select a group.'
end
redirect_to namespace_project_group_links_path(project.namespace, project) redirect_to namespace_project_group_links_path(project.namespace, project)
end end
......
...@@ -159,7 +159,8 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -159,7 +159,8 @@ class Projects::IssuesController < Projects::ApplicationController
protected protected
def issue def issue
@noteable = @issue ||= @project.issues.find_by(iid: params[:id]) || redirect_old # The Sortable default scope causes performance issues when used with find_by
@noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old
end end
alias_method :subscribable_resource, :issue alias_method :subscribable_resource, :issue
alias_method :issuable, :issue alias_method :issuable, :issue
......
...@@ -30,9 +30,15 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -30,9 +30,15 @@ class Projects::LabelsController < Projects::ApplicationController
@label = @project.labels.create(label_params) @label = @project.labels.create(label_params)
if @label.valid? if @label.valid?
redirect_to namespace_project_labels_path(@project.namespace, @project) respond_to do |format|
format.html { redirect_to namespace_project_labels_path(@project.namespace, @project) }
format.json { render json: @label }
end
else else
render 'new' respond_to do |format|
format.html { render 'new' }
format.json { render json: { message: @label.errors.messages }, status: 400 }
end
end end
end end
......
...@@ -19,6 +19,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -19,6 +19,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :define_diff_comment_vars, only: [:diffs] before_action :define_diff_comment_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines]
before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines] before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
before_action :apply_diff_view_cookie!, only: [:new_diffs]
before_action :build_merge_request, only: [:new, :new_diffs]
# Allow read any merge_request # Allow read any merge_request
before_action :authorize_read_merge_request! before_action :authorize_read_merge_request!
...@@ -210,29 +212,26 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -210,29 +212,26 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
def new def new
apply_diff_view_cookie! define_new_vars
end
build_merge_request
@noteable = @merge_request
@target_branches = if @merge_request.target_project
@merge_request.target_project.repository.branch_names
else
[]
end
@target_project = merge_request.target_project
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
@diffs = @merge_request.diffs(diff_options) if @merge_request.compare
@diff_notes_disabled = true
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses.relevant if @pipeline
@note_counts = Note.where(commit_id: @commits.map(&:id)). def new_diffs
group(:commit_id).count respond_to do |format|
format.html do
define_new_vars
render "new"
end
format.json do
@diffs = if @merge_request.can_be_created
@merge_request.diffs(diff_options)
else
[]
end
@diff_notes_disabled = true
render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) }
end
end
end end
def create def create
...@@ -490,6 +489,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -490,6 +489,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController
) )
end end
def define_new_vars
@noteable = @merge_request
@target_branches = if @merge_request.target_project
@merge_request.target_project.repository.branch_names
else
[]
end
@target_project = merge_request.target_project
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses.relevant if @pipeline
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
end
def invalid_mr def invalid_mr
# Render special view for MR with removed target branch # Render special view for MR with removed target branch
render 'invalid' render 'invalid'
...@@ -521,7 +541,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -521,7 +541,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def build_merge_request def build_merge_request
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute
end end
def compared_diff_version def compared_diff_version
......
...@@ -55,10 +55,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -55,10 +55,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end end
def destroy def destroy
@project_member = @project.members.find_by(id: params[:id]) || Members::DestroyService.new(@project, current_user, params).
@project.requesters.find_by(id: params[:id]) execute(:all)
Members::DestroyService.new(@project_member, current_user).execute
respond_to do |format| respond_to do |format|
format.html do format.html do
......
# Finder for retrieving public trending projects in a given time range.
class TrendingProjectsFinder class TrendingProjectsFinder
def execute(current_user, start_date = 1.month.ago) # current_user - The currently logged in User, if any.
projects_for(current_user).trending(start_date) # last_months - The number of months to limit the trending data to.
def execute(months_limit = 1)
Rails.cache.fetch(cache_key_for(months_limit), expires_in: 1.day) do
Project.public_only.trending(months_limit.months.ago)
end
end end
private private
def projects_for(current_user) def cache_key_for(months)
ProjectsFinder.new.execute(current_user) "trending_projects/#{months}"
end end
end end
...@@ -16,7 +16,7 @@ module AppearancesHelper ...@@ -16,7 +16,7 @@ module AppearancesHelper
end end
def brand_text def brand_text
markdown(brand_item.description) markdown_field(brand_item, :description)
end end
def brand_item def brand_item
......
...@@ -11,18 +11,6 @@ module ApplicationSettingsHelper ...@@ -11,18 +11,6 @@ module ApplicationSettingsHelper
current_application_settings.signin_enabled? current_application_settings.signin_enabled?
end end
def extra_sign_in_text
current_application_settings.sign_in_text
end
def after_sign_up_text
current_application_settings.after_sign_up_text
end
def shared_runners_text
current_application_settings.shared_runners_text
end
def user_oauth_applications? def user_oauth_applications?
current_application_settings.user_oauth_applications current_application_settings.user_oauth_applications
end end
......
...@@ -4,15 +4,18 @@ module AvatarsHelper ...@@ -4,15 +4,18 @@ module AvatarsHelper
user: commit_or_event.author, user: commit_or_event.author,
user_name: commit_or_event.author_name, user_name: commit_or_event.author_name,
user_email: commit_or_event.author_email, user_email: commit_or_event.author_email,
css_class: 'hidden-xs'
})) }))
end end
def user_avatar(options = {}) def user_avatar(options = {})
avatar_size = options[:size] || 16 avatar_size = options[:size] || 16
user_name = options[:user].try(:name) || options[:user_name] user_name = options[:user].try(:name) || options[:user_name]
css_class = options[:css_class] || ''
avatar = image_tag( avatar = image_tag(
avatar_icon(options[:user] || options[:user_email], avatar_size), avatar_icon(options[:user] || options[:user_email], avatar_size),
class: "avatar has-tooltip hidden-xs s#{avatar_size}", class: "avatar has-tooltip s#{avatar_size} #{css_class}",
alt: "#{user_name}'s avatar", alt: "#{user_name}'s avatar",
title: user_name, title: user_name,
data: { container: 'body' } data: { container: 'body' }
......
...@@ -3,7 +3,7 @@ module BroadcastMessagesHelper ...@@ -3,7 +3,7 @@ module BroadcastMessagesHelper
return unless message.present? return unless message.present?
content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do
icon('bullhorn') << ' ' << render_broadcast_message(message.message) icon('bullhorn') << ' ' << render_broadcast_message(message)
end end
end end
...@@ -32,7 +32,7 @@ module BroadcastMessagesHelper ...@@ -32,7 +32,7 @@ module BroadcastMessagesHelper
end end
end end
def render_broadcast_message(message) def render_broadcast_message(broadcast_message)
Banzai.render(message, pipeline: :broadcast_message).html_safe Banzai.render_field(broadcast_message, :message).html_safe
end end
end end
...@@ -40,8 +40,9 @@ module DropdownsHelper ...@@ -40,8 +40,9 @@ module DropdownsHelper
end end
def dropdown_toggle(toggle_text, data_attr, options = {}) def dropdown_toggle(toggle_text, data_attr, options = {})
default_label = data_attr[:default_label]
content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text") output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}")
output << icon('chevron-down') output << icon('chevron-down')
output.html_safe output.html_safe
end end
......
...@@ -13,14 +13,12 @@ module GitlabMarkdownHelper ...@@ -13,14 +13,12 @@ module GitlabMarkdownHelper
def link_to_gfm(body, url, html_options = {}) def link_to_gfm(body, url, html_options = {})
return "" if body.blank? return "" if body.blank?
escaped_body = if body.start_with?('<img') context = {
body project: @project,
else current_user: (current_user if defined?(current_user)),
escape_once(body) pipeline: :single_line,
end }
gfm_body = Banzai.render(body, context)
user = current_user if defined?(current_user)
gfm_body = Banzai.render(escaped_body, project: @project, current_user: user, pipeline: :single_line)
fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body) fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
if fragment.children.size == 1 && fragment.children[0].name == 'a' if fragment.children.size == 1 && fragment.children[0].name == 'a'
...@@ -51,17 +49,15 @@ module GitlabMarkdownHelper ...@@ -51,17 +49,15 @@ module GitlabMarkdownHelper
context[:project] ||= @project context[:project] ||= @project
html = Banzai.render(text, context) html = Banzai.render(text, context)
banzai_postprocess(html, context)
end
context.merge!( def markdown_field(object, field)
current_user: (current_user if defined?(current_user)), object = object.for_display if object.respond_to?(:for_display)
return "" unless object.present?
# RelativeLinkFilter
requested_path: @path,
project_wiki: @project_wiki,
ref: @ref
)
Banzai.post_process(html, context) html = Banzai.render_field(object, field)
banzai_postprocess(html, object.banzai_render_context(field))
end end
def asciidoc(text) def asciidoc(text)
...@@ -196,4 +192,18 @@ module GitlabMarkdownHelper ...@@ -196,4 +192,18 @@ module GitlabMarkdownHelper
icon(options[:icon]) icon(options[:icon])
end end
end end
# Calls Banzai.post_process with some common context options
def banzai_postprocess(html, context)
context.merge!(
current_user: (current_user if defined?(current_user)),
# RelativeLinkFilter
requested_path: @path,
project_wiki: @project_wiki,
ref: @ref
)
Banzai.post_process(html, context)
end
end end
...@@ -8,18 +8,12 @@ module IssuablesHelper ...@@ -8,18 +8,12 @@ module IssuablesHelper
end end
def multi_label_name(current_labels, default_label) def multi_label_name(current_labels, default_label)
# current_labels may be a string from before if current_labels && current_labels.any?
if current_labels.is_a?(Array) title = current_labels.first.try(:title)
if current_labels.count > 1 if current_labels.size > 1
"#{current_labels[0]} +#{current_labels.count - 1} more" "#{title} +#{current_labels.size - 1} more"
else else
current_labels[0] title
end
elsif current_labels.is_a?(String)
if current_labels.nil? || current_labels.empty?
default_label
else
current_labels
end end
else else
default_label default_label
......
...@@ -113,14 +113,13 @@ module IssuesHelper ...@@ -113,14 +113,13 @@ module IssuesHelper
end end
end end
def award_user_list(awards, current_user) def award_user_list(awards, current_user, limit: 10)
names = awards.map do |award| names = awards.map do |award|
award.user == current_user ? 'You' : award.user.name award.user == current_user ? 'You' : award.user.name
end end
# Take first 9 OR current user + first 9
current_user_name = names.delete('You') current_user_name = names.delete('You')
names = names.first(9).insert(0, current_user_name).compact names = names.insert(0, current_user_name).compact.first(limit)
names << "#{awards.size - names.size} more." if awards.size > names.size names << "#{awards.size - names.size} more." if awards.size > names.size
......
...@@ -115,8 +115,9 @@ module LabelsHelper ...@@ -115,8 +115,9 @@ module LabelsHelper
end end
def labels_filter_path def labels_filter_path
if @project project = @target_project || @project
namespace_project_labels_path(@project.namespace, @project, :json) if project
namespace_project_labels_path(project.namespace, project, :json)
else else
dashboard_labels_path(:json) dashboard_labels_path(:json)
end end
......
...@@ -71,8 +71,9 @@ module MilestonesHelper ...@@ -71,8 +71,9 @@ module MilestonesHelper
end end
def milestones_filter_dropdown_path def milestones_filter_dropdown_path
if @project project = @target_project || @project
namespace_project_milestones_path(@project.namespace, @project, :json) if project
namespace_project_milestones_path(project.namespace, project, :json)
else else
dashboard_milestones_path(:json) dashboard_milestones_path(:json)
end end
......
...@@ -92,12 +92,8 @@ module PageLayoutHelper ...@@ -92,12 +92,8 @@ module PageLayoutHelper
end end
end end
def fluid_layout(enabled = false) def fluid_layout
if @fluid_layout.nil? current_user && current_user.layout == "fluid"
@fluid_layout = (current_user && current_user.layout == "fluid") || enabled
else
@fluid_layout
end
end end
def blank_container(enabled = false) def blank_container(enabled = false)
......
...@@ -153,8 +153,18 @@ module SearchHelper ...@@ -153,8 +153,18 @@ module SearchHelper
search_path(options) search_path(options)
end end
# Sanitize html generated after parsing markdown from issue description or comment # Sanitize a HTML field for search display. Most tags are stripped out and the
def search_md_sanitize(html) # maximum length is set to 200 characters.
def search_md_sanitize(object, field)
html = markdown_field(object, field)
html = Truncato.truncate(
html,
count_tags: false,
count_tail: false,
max_length: 200
)
# Truncato's filtered_tags and filtered_attributes are not quite the same
sanitize(html, tags: %w(a p ol ul li pre code)) sanitize(html, tags: %w(a p ol ul li pre code))
end end
end end
...@@ -49,12 +49,10 @@ module SelectsHelper ...@@ -49,12 +49,10 @@ module SelectsHelper
end end
def select2_tag(id, opts = {}) def select2_tag(id, opts = {})
css_class = '' opts[:class] << ' multiselect' if opts[:multiple]
css_class << 'multiselect ' if opts[:multiple]
css_class << (opts[:class] || '')
value = opts[:selected] || '' value = opts[:selected] || ''
hidden_field_tag(id, value, class: css_class) hidden_field_tag(id, value, opts)
end end
private private
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment