summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG22
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.lock7
-rw-r--r--app/assets/javascripts/abuse_reports.js.es639
-rw-r--r--app/assets/javascripts/application.js7
-rw-r--r--app/assets/javascripts/awards_handler.js54
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es657
-rw-r--r--app/assets/javascripts/boards/components/board.js.es685
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js.es649
-rw-r--r--app/assets/javascripts/boards/components/board_card.js.es643
-rw-r--r--app/assets/javascripts/boards/components/board_delete.js.es619
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es689
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js.es654
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js.es625
-rw-r--r--app/assets/javascripts/boards/models/issue.js.es644
-rw-r--r--app/assets/javascripts/boards/models/label.js.es69
-rw-r--r--app/assets/javascripts/boards/models/list.js.es6125
-rw-r--r--app/assets/javascripts/boards/models/user.js.es68
-rw-r--r--app/assets/javascripts/boards/services/board_service.js.es661
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js.es6112
-rwxr-xr-xapp/assets/javascripts/boards/test_utils/simulate_drag.js119
-rw-r--r--app/assets/javascripts/boards/vue_resource_interceptor.js.es610
-rw-r--r--app/assets/javascripts/build.js35
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js1
-rw-r--r--app/assets/javascripts/create_label.js.es6126
-rw-r--r--app/assets/javascripts/dispatcher.js5
-rw-r--r--app/assets/javascripts/labels_select.js90
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js5
-rw-r--r--app/assets/javascripts/merge_conflict_data_provider.js.es6341
-rw-r--r--app/assets/javascripts/merge_conflict_resolver.js.es685
-rw-r--r--app/assets/javascripts/merge_request_tabs.js27
-rw-r--r--app/assets/javascripts/merge_request_widget.js4
-rw-r--r--app/assets/javascripts/milestone_select.js8
-rw-r--r--app/assets/javascripts/pipeline.js.es615
-rw-r--r--app/assets/javascripts/users_select.js9
-rw-r--r--app/assets/stylesheets/behaviors.scss5
-rw-r--r--app/assets/stylesheets/framework/buttons.scss4
-rw-r--r--app/assets/stylesheets/framework/mixins.scss7
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss4
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/assets/stylesheets/highlight/dark.scss7
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss7
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss7
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss13
-rw-r--r--app/assets/stylesheets/highlight/white.scss13
-rw-r--r--app/assets/stylesheets/mailers/repository_push_email.scss5
-rw-r--r--app/assets/stylesheets/pages/admin.scss42
-rw-r--r--app/assets/stylesheets/pages/boards.scss329
-rw-r--r--app/assets/stylesheets/pages/builds.scss88
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss238
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss193
-rw-r--r--app/assets/stylesheets/pages/profile.scss6
-rw-r--r--app/controllers/dashboard/todos_controller.rb6
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb12
-rw-r--r--app/controllers/profiles/u2f_registrations_controller.rb7
-rw-r--r--app/controllers/projects/application_controller.rb1
-rw-r--r--app/controllers/projects/board_lists_controller.rb65
-rw-r--r--app/controllers/projects/boards/application_controller.rb15
-rw-r--r--app/controllers/projects/boards/issues_controller.rb56
-rw-r--r--app/controllers/projects/boards/lists_controller.rb81
-rw-r--r--app/controllers/projects/boards_controller.rb15
-rw-r--r--app/controllers/projects/merge_requests_controller.rb75
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/blob_helper.rb9
-rw-r--r--app/helpers/ci_status_helper.rb26
-rw-r--r--app/helpers/commits_helper.rb45
-rw-r--r--app/helpers/issues_helper.rb14
-rw-r--r--app/helpers/nav_helper.rb1
-rw-r--r--app/mailers/emails/issues.rb5
-rw-r--r--app/mailers/emails/merge_requests.rb5
-rw-r--r--app/models/ability.rb5
-rw-r--r--app/models/board.rb7
-rw-r--r--app/models/ci/build.rb2
-rw-r--r--app/models/ci/pipeline.rb4
-rw-r--r--app/models/diff_note.rb2
-rw-r--r--app/models/label.rb2
-rw-r--r--app/models/list.rb34
-rw-r--r--app/models/merge_request.rb37
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/repository.rb28
-rw-r--r--app/models/u2f_registration.rb7
-rw-r--r--app/services/boards/base_service.rb5
-rw-r--r--app/services/boards/create_service.rb16
-rw-r--r--app/services/boards/issues/list_service.rb63
-rw-r--r--app/services/boards/issues/move_service.rb59
-rw-r--r--app/services/boards/lists/create_service.rb22
-rw-r--r--app/services/boards/lists/destroy_service.rb25
-rw-r--r--app/services/boards/lists/generate_service.rb36
-rw-r--r--app/services/boards/lists/move_service.rb51
-rw-r--r--app/services/issuable_base_service.rb3
-rw-r--r--app/services/issues/update_service.rb7
-rw-r--r--app/services/merge_requests/resolve_service.rb31
-rw-r--r--app/services/merge_requests/update_service.rb11
-rw-r--r--app/services/notification_service.rb39
-rw-r--r--app/services/todo_service.rb6
-rw-r--r--app/views/admin/abuse_reports/_abuse_report.html.haml17
-rw-r--r--app/views/admin/abuse_reports/index.html.haml33
-rw-r--r--app/views/layouts/_page.html.haml5
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/notify/new_mention_in_issue_email.html.haml12
-rw-r--r--app/views/notify/new_mention_in_issue_email.text.erb7
-rw-r--r--app/views/notify/new_mention_in_merge_request_email.html.haml15
-rw-r--r--app/views/notify/new_mention_in_merge_request_email.text.erb9
-rw-r--r--app/views/notify/repository_push_email.html.haml2
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml31
-rw-r--r--app/views/projects/boards/components/_blank_state.html.haml15
-rw-r--r--app/views/projects/boards/components/_board.html.haml44
-rw-r--r--app/views/projects/boards/components/_card.html.haml34
-rw-r--r--app/views/projects/boards/show.html.haml19
-rw-r--r--app/views/projects/builds/_sidebar.html.haml197
-rw-r--r--app/views/projects/builds/show.html.haml28
-rw-r--r--app/views/projects/buttons/_star.html.haml6
-rw-r--r--app/views/projects/ci/builds/_build_pipeline.html.haml14
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml20
-rw-r--r--app/views/projects/commit/_pipeline.html.haml22
-rw-r--r--app/views/projects/commit/_pipelines_list.haml17
-rw-r--r--app/views/projects/diffs/_file.html.haml7
-rw-r--r--app/views/projects/diffs/_line.html.haml7
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml9
-rw-r--r--app/views/projects/issues/_head.html.haml5
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml11
-rw-r--r--app/views/projects/merge_requests/_show.html.haml14
-rw-r--r--app/views/projects/merge_requests/conflicts.html.haml29
-rw-r--r--app/views/projects/merge_requests/conflicts/_commit_stats.html.haml20
-rw-r--r--app/views/projects/merge_requests/conflicts/_inline_view.html.haml28
-rw-r--r--app/views/projects/merge_requests/conflicts/_parallel_view.html.haml27
-rw-r--r--app/views/projects/merge_requests/conflicts/_submit_form.html.haml15
-rw-r--r--app/views/projects/merge_requests/show/_builds.html.haml1
-rw-r--r--app/views/projects/merge_requests/show/_pipelines.html.haml1
-rw-r--r--app/views/projects/merge_requests/widget/_merged.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/_open.html.haml10
-rw-r--r--app/views/projects/merge_requests/widget/_show.html.haml3
-rw-r--r--app/views/projects/merge_requests/widget/open/_conflicts.html.haml13
-rw-r--r--app/views/shared/icons/_icon_fork.svg2
-rw-r--r--app/views/shared/issuable/_filter.html.haml12
-rw-r--r--app/views/shared/issuable/_form.html.haml2
-rw-r--r--app/views/shared/issuable/_label_page_default.html.haml7
-rw-r--r--app/views/shared/projects/_project.html.haml4
-rw-r--r--app/views/u2f/_register.html.haml13
-rw-r--r--app/workers/repository_fork_worker.rb4
-rw-r--r--app/workers/repository_import_worker.rb4
-rw-r--r--config/application.rb2
-rw-r--r--config/routes.rb19
-rw-r--r--db/fixtures/development/14_builds.rb41
-rw-r--r--db/migrate/20160727191041_create_boards.rb13
-rw-r--r--db/migrate/20160727193336_create_lists.rb16
-rw-r--r--db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb15
-rw-r--r--db/migrate/20160816161312_add_column_name_to_u2f_registrations.rb29
-rw-r--r--db/schema.rb27
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/builds.md46
-rw-r--r--doc/api/deployments.md218
-rw-r--r--doc/api/pipelines.md207
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--doc/legal/corporate_contributor_license_agreement.md4
-rw-r--r--doc/monitoring/performance/influxdb_schema.md9
-rw-r--r--doc/user/project/description_templates.md42
-rw-r--r--doc/user/project/img/description_templates.pngbin0 -> 20444 bytes
-rw-r--r--doc/user/project/merge_requests/img/conflict_section.pngbin0 -> 247537 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_request_widget.pngbin0 -> 32292 bytes
-rw-r--r--doc/user/project/merge_requests/resolve_conflicts.md41
-rw-r--r--doc/workflow/README.md2
-rw-r--r--doc/workflow/description_templates.md12
-rw-r--r--doc/workflow/img/description_templates.pngbin57670 -> 0 bytes
-rw-r--r--doc/workflow/notifications.md7
-rw-r--r--features/steps/project/issues/award_emoji.rb2
-rw-r--r--features/steps/project/issues/issues.rb6
-rw-r--r--features/steps/project/merge_requests.rb3
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/builds.rb21
-rw-r--r--lib/api/deployments.rb40
-rw-r--r--lib/api/entities.rb20
-rw-r--r--lib/api/pipelines.rb74
-rw-r--r--lib/ci/api/builds.rb8
-rw-r--r--lib/gitlab/conflict/file.rb186
-rw-r--r--lib/gitlab/conflict/file_collection.rb57
-rw-r--r--lib/gitlab/conflict/parser.rb62
-rw-r--r--lib/gitlab/diff/line.rb20
-rw-r--r--lib/gitlab/downtime_check/message.rb19
-rw-r--r--lib/gitlab/metrics.rb9
-rw-r--r--lib/gitlab/metrics/metric.rb9
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb4
-rw-r--r--lib/gitlab/metrics/sidekiq_middleware.rb4
-rw-r--r--lib/gitlab/metrics/transaction.rb21
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb120
-rw-r--r--spec/controllers/projects/boards/lists_controller_spec.rb241
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb43
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb136
-rw-r--r--spec/factories/boards.rb5
-rw-r--r--spec/factories/ci/pipelines.rb18
-rw-r--r--spec/factories/lists.rb20
-rw-r--r--spec/factories/projects.rb6
-rw-r--r--spec/features/boards/boards_spec.rb622
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb72
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb19
-rw-r--r--spec/features/merge_requests/pipelines_spec.rb48
-rw-r--r--spec/features/projects/issues/list_spec.rb20
-rw-r--r--spec/features/projects/merge_requests/list_spec.rb20
-rw-r--r--spec/features/projects/pipelines_spec.rb6
-rw-r--r--spec/features/u2f_spec.rb36
-rw-r--r--spec/fixtures/api/schemas/issue.json40
-rw-r--r--spec/fixtures/api/schemas/issues.json4
-rw-r--r--spec/fixtures/api/schemas/list.json39
-rw-r--r--spec/fixtures/api/schemas/lists.json4
-rw-r--r--spec/helpers/blob_helper_spec.rb26
-rw-r--r--spec/helpers/issues_helper_spec.rb26
-rw-r--r--spec/javascripts/abuse_reports_spec.js.es641
-rw-r--r--spec/javascripts/awards_handler_spec.js46
-rw-r--r--spec/javascripts/boards/boards_store_spec.js.es6164
-rw-r--r--spec/javascripts/boards/issue_spec.js.es683
-rw-r--r--spec/javascripts/boards/list_spec.js.es689
-rw-r--r--spec/javascripts/boards/mock_data.js.es653
-rw-r--r--spec/javascripts/fixtures/abuse_reports.html.haml16
-rw-r--r--spec/lib/gitlab/conflict/file_collection_spec.rb24
-rw-r--r--spec/lib/gitlab/conflict/file_spec.rb261
-rw-r--r--spec/lib/gitlab/conflict/parser_spec.rb188
-rw-r--r--spec/lib/gitlab/downtime_check/message_spec.rb26
-rw-r--r--spec/lib/gitlab/metrics/metric_spec.rb18
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb9
-rw-r--r--spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb24
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb57
-rw-r--r--spec/lib/gitlab/metrics_spec.rb24
-rw-r--r--spec/models/board_spec.rb12
-rw-r--r--spec/models/label_spec.rb2
-rw-r--r--spec/models/list_spec.rb117
-rw-r--r--spec/models/merge_request_spec.rb79
-rw-r--r--spec/models/project_spec.rb1
-rw-r--r--spec/requests/api/builds_spec.rb23
-rw-r--r--spec/requests/api/deployments_spec.rb60
-rw-r--r--spec/requests/api/environments_spec.rb1
-rw-r--r--spec/requests/api/pipelines_spec.rb133
-rw-r--r--spec/services/boards/create_service_spec.rb35
-rw-r--r--spec/services/boards/issues/list_service_spec.rb73
-rw-r--r--spec/services/boards/issues/move_service_spec.rb140
-rw-r--r--spec/services/boards/lists/create_service_spec.rb54
-rw-r--r--spec/services/boards/lists/destroy_service_spec.rb47
-rw-r--r--spec/services/boards/lists/generate_service_spec.rb40
-rw-r--r--spec/services/boards/lists/move_service_spec.rb110
-rw-r--r--spec/services/issues/update_service_spec.rb5
-rw-r--r--spec/services/merge_requests/update_service_spec.rb5
-rw-r--r--spec/services/notification_service_spec.rb38
-rw-r--r--spec/services/todo_service_spec.rb30
-rw-r--r--spec/support/api/schema_matcher.rb8
-rw-r--r--spec/support/email_helpers.rb10
-rw-r--r--spec/support/fake_u2f_device.rb5
-rw-r--r--spec/support/test_env.rb44
-rw-r--r--spec/support/updating_mentions_shared_examples.rb32
-rw-r--r--vendor/assets/javascripts/Sortable.js1285
-rw-r--r--vendor/assets/javascripts/clipboard.js20
-rw-r--r--vendor/assets/javascripts/vue-resource.full.js1318
-rw-r--r--vendor/assets/javascripts/vue-resource.js.erb2
-rw-r--r--vendor/assets/javascripts/vue-resource.min.js7
-rw-r--r--vendor/assets/javascripts/vue.full.js10073
-rw-r--r--vendor/assets/javascripts/vue.js.erb2
-rw-r--r--vendor/assets/javascripts/vue.min.js9
255 files changed, 21847 insertions, 494 deletions
diff --git a/CHANGELOG b/CHANGELOG
index f1684e9f3f9..cd9c603e798 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -5,10 +5,13 @@ v 8.11.0 (unreleased)
- Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar)
- Ability to specify branches for Pivotal Tracker integration (Egor Lynko)
- Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres)
+ - Add delimiter to project stars and forks count (ClemMakesApps)
- Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres)
- Fix the title of the toggle dropdown button. !5515 (herminiotorres)
- Rename `markdown_preview` routes to `preview_markdown`. (Christopher Bartz)
- Update to Ruby 2.3.1. !4948
+ - Add Issues Board !5548
+ - Allow resolving merge conflicts in the UI !5479
- Improve diff performance by eliminating redundant checks for text blobs
- Ensure that branch names containing escapable characters (e.g. %20) aren't unescaped indiscriminately. !5770 (ewiltshi)
- Convert switch icon into icon font (ClemMakesApps)
@@ -16,7 +19,9 @@ v 8.11.0 (unreleased)
- API: List access requests, request access, approve, and deny access requests to a project or a group. !4833
- Use long options for curl examples in documentation !5703 (winniehell)
- Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell)
+ - GitLab Performance Monitoring can now track custom events such as the number of tags pushed to a repository
- Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell)
+ - Allow naming U2F devices !5833
- Ignore URLs starting with // in Markdown links !5677 (winniehell)
- Fix CI status icon link underline (ClemMakesApps)
- The Repository class is now instrumented
@@ -25,12 +30,15 @@ v 8.11.0 (unreleased)
- Expand commit message width in repo view (ClemMakesApps)
- Cache highlighted diff lines for merge requests
- Pre-create all builds for a Pipeline when the new Pipeline is created !5295
+ - API: Add deployment endpoints
+ - API: Add Play endpoint on Builds
- Fix of 'Commits being passed to custom hooks are already reachable when using the UI'
- Show member roles to all users on members page
- Project.visible_to_user is instrumented again
- Fix awardable button mutuality loading spinners (ClemMakesApps)
- Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable
- Optimize maximum user access level lookup in loading of notes
+ - Send notification emails to users newly mentioned in issue and MR edits !5800
- Add "No one can push" as an option for protected branches. !5081
- Improve performance of AutolinkFilter#text_parse by using XPath
- Add experimental Redis Sentinel support !1877
@@ -42,12 +50,15 @@ v 8.11.0 (unreleased)
- Remove unused images (ClemMakesApps)
- Get issue and merge request description templates from repositories
- Add hover state to todos !5361 (winniehell)
+ - Fix icon alignment of star and fork buttons !5451 (winniehell)
- Limit git rev-list output count to one in forced push check
- Show deployment status on merge requests with external URLs
- Clean up unused routes (Josef Strzibny)
- Fix issue on empty project to allow developers to only push to protected branches if given permission
+ - API: Add enpoints for pipelines
- Add green outline to New Branch button. !5447 (winniehell)
- Optimize generating of cache keys for issues and notes
+ - Fix repository push email formatting in Outlook
- Improve performance of syntax highlighting Markdown code blocks
- Update to gitlab_git 10.4.1 and take advantage of preserved Ref objects
- Remove delay when hitting "Reply..." button on page with a lot of discussions
@@ -77,6 +88,8 @@ v 8.11.0 (unreleased)
- Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le)
- Load project invited groups and members eagerly in `ProjectTeam#fetch_members`
- Add pipeline events hook
+ - Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
+ - Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison)
- Bump gitlab_git to speedup DiffCollection iterations
- Rewrite description of a blocked user in admin settings. (Elias Werberich)
- Make branches sortable without push permission !5462 (winniehell)
@@ -91,15 +104,19 @@ v 8.11.0 (unreleased)
- Allow branch names ending with .json for graph and network page !5579 (winniehell)
- Add the `sprockets-es6` gem
- Improve OAuth2 client documentation (muteor)
+ - Fix diff comments inverted toggle bug (ClemMakesApps)
- Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska)
- Profile requests when a header is passed
- Avoid calculation of line_code and position for _line partial when showing diff notes on discussion tab.
- Speedup DiffNote#active? on discussions, preloading noteables and avoid touching git repository to return diff_refs when possible
- Add commit stats in commit api. !5517 (dixpac)
- Add CI configuration button on project page
+ - Fix merge request new view not changing code view rendering style
+ - edit_blob_link will use blob passed onto the options parameter
- Make error pages responsive (Takuya Noguchi)
- The performance of the project dropdown used for moving issues has been improved
- Fix skip_repo parameter being ignored when destroying a namespace
+ - Add all builds into stage/job dropdowns on builds page
- Change requests_profiles resource constraint to catch virtually any file
- Bump gitlab_git to lazy load compare commits
- Reduce number of queries made for merge_requests/:id/diffs
@@ -112,15 +129,20 @@ v 8.11.0 (unreleased)
- Speed up and reduce memory usage of Commit#repo_changes, Repository#expire_avatar_cache and IrkerWorker
- Add unfold links for Side-by-Side view. !5415 (Tim Masliuchenko)
- Adds support for pending invitation project members importing projects
+ - Add pipeline visualization/graph on pipeline page
- Update devise initializer to turn on changed password notification emails. !5648 (tombell)
- Avoid to show the original password field when password is automatically set. !5712 (duduribeiro)
- Fix importing GitLab projects with an invalid MR source project
- Sort folders with submodules in Files view !5521
- Each `File::exists?` replaced to `File::exist?` because of deprecate since ruby version 2.2.0
- Add auto-completition in pipeline (Katarzyna Kobierska Ula Budziszewska)
+ - Add pipelines tab to merge requests
- Fix a memory leak caused by Banzai::Filter::SanitizationFilter
- Speed up todos queries by limiting the projects set we join with
- Ensure file editing in UI does not overwrite commited changes without warning user
+ - Eliminate unneeded calls to Repository#blob_at when listing commits with no path
+ - Update gitlab_git gem to 10.4.7
+ - Simplify SQL queries of marking a todo as done
v 8.10.6
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
diff --git a/Gemfile b/Gemfile
index a99865a6f66..44d2fe557f1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -53,7 +53,7 @@ gem 'browser', '~> 2.2'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem 'gitlab_git', '~> 10.4.5'
+gem 'gitlab_git', '~> 10.4.7'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -314,6 +314,7 @@ end
group :test do
gem 'shoulda-matchers', '~> 2.8.0', require: false
gem 'email_spec', '~> 1.6.0'
+ gem 'json-schema', '~> 2.6.2'
gem 'webmock', '~> 1.21.0'
gem 'test_after_commit', '~> 0.4.2'
gem 'sham_rack', '~> 1.3.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index 3c7b3317a10..ff95fb4ceed 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -278,7 +278,7 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
- gitlab_git (10.4.5)
+ gitlab_git (10.4.7)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
@@ -356,6 +356,8 @@ GEM
jquery-ui-rails (5.0.5)
railties (>= 3.2.16)
json (1.8.3)
+ json-schema (2.6.2)
+ addressable (~> 2.3.8)
jwt (1.5.4)
kaminari (0.17.0)
actionpack (>= 3.0.0)
@@ -855,7 +857,7 @@ DEPENDENCIES
github-linguist (~> 4.7.0)
github-markup (~> 1.4)
gitlab-flowdock-git-hook (~> 1.0.1)
- gitlab_git (~> 10.4.5)
+ gitlab_git (~> 10.4.7)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
@@ -873,6 +875,7 @@ DEPENDENCIES
jquery-rails (~> 4.1.0)
jquery-turbolinks (~> 2.1.0)
jquery-ui-rails (~> 5.0.0)
+ json-schema (~> 2.6.2)
jwt
kaminari (~> 0.17.0)
knapsack (~> 1.11.0)
diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6
new file mode 100644
index 00000000000..748084b0307
--- /dev/null
+++ b/app/assets/javascripts/abuse_reports.js.es6
@@ -0,0 +1,39 @@
+window.gl = window.gl || {};
+((global) => {
+ const MAX_MESSAGE_LENGTH = 500;
+ const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
+
+ class AbuseReports {
+ constructor() {
+ $(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage);
+ $(document)
+ .off('click', MESSAGE_CELL_SELECTOR)
+ .on('click', MESSAGE_CELL_SELECTOR, this.toggleMessageTruncation);
+ }
+
+ truncateLongMessage() {
+ const $messageCellElement = $(this);
+ const reportMessage = $messageCellElement.text();
+ if (reportMessage.length > MAX_MESSAGE_LENGTH) {
+ $messageCellElement.data('original-message', reportMessage);
+ $messageCellElement.data('message-truncated', 'true');
+ $messageCellElement.text(global.text.truncate(reportMessage, MAX_MESSAGE_LENGTH));
+ }
+ }
+
+ toggleMessageTruncation() {
+ const $messageCellElement = $(this);
+ const originalMessage = $messageCellElement.data('original-message');
+ if (!originalMessage) return;
+ if ($messageCellElement.data('message-truncated') === 'true') {
+ $messageCellElement.data('message-truncated', 'false');
+ $messageCellElement.text(originalMessage);
+ } else {
+ $messageCellElement.data('message-truncated', 'true');
+ $messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`);
+ }
+ }
+ }
+
+ global.AbuseReports = AbuseReports;
+})(window.gl);
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 0d6b7b16002..f6e8e770527 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -224,8 +224,11 @@
return $('.navbar-toggle').toggleClass('active');
});
$body.on("click", ".js-toggle-diff-comments", function(e) {
- $(this).toggleClass('active');
- $(this).closest(".diff-file").find(".notes_holder").toggle();
+ var $this = $(this);
+ var showComments = $this.hasClass('active');
+
+ $this.toggleClass('active');
+ $this.closest(".diff-file").find(".notes_holder").toggle(showComments);
return e.preventDefault();
});
$document.off("click", '.js-confirm-danger');
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 2c5b83e4f1e..aee1c29eee3 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,5 +1,6 @@
(function() {
this.AwardsHandler = (function() {
+ const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence
function AwardsHandler() {
this.aliases = gl.emojiAliases();
$(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) {
@@ -130,7 +131,7 @@
counter = $emojiButton.find('.js-counter');
counter.text(parseInt(counter.text()) + 1);
$emojiButton.addClass('active');
- this.addMeToUserList(votesBlock, emoji);
+ this.addYouToUserList(votesBlock, emoji);
return this.animateEmoji($emojiButton);
}
} else {
@@ -176,11 +177,11 @@
counterNumber = parseInt(counter.text(), 10);
if (counterNumber > 1) {
counter.text(counterNumber - 1);
- this.removeMeFromUserList($emojiButton, emoji);
+ this.removeYouFromUserList($emojiButton, emoji);
} else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
$emojiButton.tooltip('destroy');
counter.text('0');
- this.removeMeFromUserList($emojiButton, emoji);
+ this.removeYouFromUserList($emojiButton, emoji);
if ($emojiButton.parents('.note').length) {
this.removeEmoji($emojiButton);
}
@@ -204,43 +205,48 @@
return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || '';
};
- AwardsHandler.prototype.removeMeFromUserList = function($emojiButton, emoji) {
+ AwardsHandler.prototype.toSentence = function(list) {
+ if(list.length <= 2){
+ return list.join(' and ');
+ }
+ else{
+ return list.slice(0, -1).join(', ') + ', and ' + list[list.length - 1];
+ }
+ };
+
+ AwardsHandler.prototype.removeYouFromUserList = function($emojiButton, emoji) {
var authors, awardBlock, newAuthors, originalTitle;
awardBlock = $emojiButton;
originalTitle = this.getAwardTooltip(awardBlock);
- authors = originalTitle.split(', ');
- authors.splice(authors.indexOf('me'), 1);
- newAuthors = authors.join(', ');
- awardBlock.closest('.js-emoji-btn').removeData('original-title').attr('data-original-title', newAuthors);
- return this.resetTooltip(awardBlock);
+ authors = originalTitle.split(FROM_SENTENCE_REGEX);
+ authors.splice(authors.indexOf('You'), 1);
+ return awardBlock
+ .closest('.js-emoji-btn')
+ .removeData('title')
+ .removeAttr('data-title')
+ .removeAttr('data-original-title')
+ .attr('title', this.toSentence(authors))
+ .tooltip('fixTitle');
};
- AwardsHandler.prototype.addMeToUserList = function(votesBlock, emoji) {
+ AwardsHandler.prototype.addYouToUserList = function(votesBlock, emoji) {
var awardBlock, origTitle, users;
awardBlock = this.findEmojiIcon(votesBlock, emoji).parent();
origTitle = this.getAwardTooltip(awardBlock);
users = [];
if (origTitle) {
- users = origTitle.trim().split(', ');
+ users = origTitle.trim().split(FROM_SENTENCE_REGEX);
}
- users.push('me');
- awardBlock.attr('title', users.join(', '));
- return this.resetTooltip(awardBlock);
- };
-
- AwardsHandler.prototype.resetTooltip = function(award) {
- var cb;
- award.tooltip('destroy');
- cb = function() {
- return award.tooltip();
- };
- return setTimeout(cb, 200);
+ users.unshift('You');
+ return awardBlock
+ .attr('title', this.toSentence(users))
+ .tooltip('fixTitle');
};
AwardsHandler.prototype.createEmoji_ = function(votesBlock, emoji) {
var $emojiButton, buttonHtml, emojiCssClass;
emojiCssClass = this.resolveNameToCssClass(emoji);
- buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'> <div class='icon emoji-icon " + emojiCssClass + "' data-emoji='" + emoji + "'></div> <span class='award-control-text js-counter'>1</span> </button>";
+ buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='You' data-placement='bottom'> <div class='icon emoji-icon " + emojiCssClass + "' data-emoji='" + emoji + "'></div> <span class='award-control-text js-counter'>1</span> </button>";
$emojiButton = $(buttonHtml);
$emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('emoji', emoji);
this.animateEmoji($emojiButton);
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
new file mode 100644
index 00000000000..2c65d4427be
--- /dev/null
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -0,0 +1,57 @@
+//= require vue
+//= require vue-resource
+//= require Sortable
+//= require_tree ./models
+//= require_tree ./stores
+//= require_tree ./services
+//= require_tree ./mixins
+//= require ./components/board
+//= require ./components/new_list_dropdown
+//= require ./vue_resource_interceptor
+
+$(() => {
+ const $boardApp = document.getElementById('board-app'),
+ Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+
+ if (gl.IssueBoardsApp) {
+ gl.IssueBoardsApp.$destroy(true);
+ }
+
+ gl.IssueBoardsApp = new Vue({
+ el: $boardApp,
+ components: {
+ 'board': gl.issueBoards.Board
+ },
+ data: {
+ state: Store.state,
+ loading: true,
+ endpoint: $boardApp.dataset.endpoint,
+ disabled: $boardApp.dataset.disabled === 'true',
+ issueLinkBase: $boardApp.dataset.issueLinkBase
+ },
+ init: Store.create.bind(Store),
+ created () {
+ gl.boardService = new BoardService(this.endpoint);
+ },
+ ready () {
+ Store.disabled = this.disabled;
+ gl.boardService.all()
+ .then((resp) => {
+ resp.json().forEach((board) => {
+ const list = Store.addList(board);
+
+ if (list.type === 'done') {
+ list.position = Infinity;
+ } else if (list.type === 'backlog') {
+ list.position = -1;
+ }
+ });
+
+ Store.addBlankState();
+ this.loading = false;
+ });
+ }
+ });
+});
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
new file mode 100644
index 00000000000..e17784e7948
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -0,0 +1,85 @@
+//= require ./board_blank_state
+//= require ./board_delete
+//= require ./board_list
+
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.Board = Vue.extend({
+ components: {
+ 'board-list': gl.issueBoards.BoardList,
+ 'board-delete': gl.issueBoards.BoardDelete,
+ 'board-blank-state': gl.issueBoards.BoardBlankState
+ },
+ props: {
+ list: Object,
+ disabled: Boolean,
+ issueLinkBase: String
+ },
+ data () {
+ return {
+ query: '',
+ filters: Store.state.filters
+ };
+ },
+ watch: {
+ query () {
+ this.list.filters = this.getFilterData();
+ this.list.getIssues(true);
+ },
+ filters: {
+ handler () {
+ this.list.page = 1;
+ this.list.getIssues(true);
+ },
+ deep: true
+ }
+ },
+ methods: {
+ getFilterData () {
+ const filters = this.filters;
+ let queryData = { search: this.query };
+
+ Object.keys(filters).forEach((key) => { queryData[key] = filters[key]; });
+
+ return queryData;
+ }
+ },
+ ready () {
+ const options = gl.issueBoards.getBoardSortableDefaultOptions({
+ disabled: this.disabled,
+ group: 'boards',
+ draggable: '.is-draggable',
+ handle: '.js-board-handle',
+ onEnd: (e) => {
+ document.body.classList.remove('is-dragging');
+
+ if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
+ const order = this.sortable.toArray(),
+ $board = this.$parent.$refs.board[e.oldIndex + 1],
+ list = $board.list;
+
+ $board.$destroy(true);
+
+ this.$nextTick(() => {
+ Store.state.lists.splice(e.newIndex, 0, list);
+ Store.moveList(list, order);
+ });
+ }
+ }
+ });
+
+ if (bp.getBreakpointSize() === 'xs') {
+ options.handle = '.js-board-drag-handle';
+ }
+
+ this.sortable = Sortable.create(this.$el.parentNode, options);
+ },
+ beforeDestroy () {
+ Store.state.lists.$remove(this.list);
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6
new file mode 100644
index 00000000000..63d72d857d9
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6
@@ -0,0 +1,49 @@
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardBlankState = Vue.extend({
+ data () {
+ return {
+ predefinedLabels: [
+ new ListLabel({ title: 'Development', color: '#5CB85C' }),
+ new ListLabel({ title: 'Testing', color: '#F0AD4E' }),
+ new ListLabel({ title: 'Production', color: '#FF5F00' }),
+ new ListLabel({ title: 'Ready', color: '#FF0000' })
+ ]
+ }
+ },
+ methods: {
+ addDefaultLists () {
+ this.clearBlankState();
+
+ this.predefinedLabels.forEach((label, i) => {
+ Store.addList({
+ title: label.title,
+ position: i,
+ list_type: 'label',
+ label: {
+ title: label.title,
+ color: label.color
+ }
+ });
+ });
+
+ // Save the labels
+ gl.boardService.generateDefaultLists()
+ .then((resp) => {
+ resp.json().forEach((listObj) => {
+ const list = Store.findList('title', listObj.title);
+
+ list.id = listObj.id;
+ list.label.id = listObj.label.id;
+ list.getIssues();
+ });
+ });
+ },
+ clearBlankState: Store.removeBlankState.bind(Store)
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6
new file mode 100644
index 00000000000..4a7cfeaeab2
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_card.js.es6
@@ -0,0 +1,43 @@
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardCard = Vue.extend({
+ props: {
+ list: Object,
+ issue: Object,
+ issueLinkBase: String,
+ disabled: Boolean,
+ index: Number
+ },
+ methods: {
+ filterByLabel (label, e) {
+ let labelToggleText = label.title;
+ const labelIndex = Store.state.filters['label_name'].indexOf(label.title);
+ $(e.target).tooltip('hide');
+
+ if (labelIndex === -1) {
+ Store.state.filters['label_name'].push(label.title);
+ $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`);
+ } else {
+ Store.state.filters['label_name'].splice(labelIndex, 1);
+ labelToggleText = Store.state.filters['label_name'][0];
+ $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove();
+ }
+
+ const selectedLabels = Store.state.filters['label_name'];
+ if (selectedLabels.length === 0) {
+ labelToggleText = 'Label';
+ } else if (selectedLabels.length > 1) {
+ labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`;
+ }
+
+ $('.labels-filter .dropdown-toggle-text').text(labelToggleText);
+
+ Store.updateFiltersUrl();
+ }
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js.es6
new file mode 100644
index 00000000000..34653cd48ef
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_delete.js.es6
@@ -0,0 +1,19 @@
+(() => {
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardDelete = Vue.extend({
+ props: {
+ list: Object
+ },
+ methods: {
+ deleteBoard () {
+ $(this.$el).tooltip('hide');
+
+ if (confirm('Are you sure you want to delete this list?')) {
+ this.list.destroy();
+ }
+ }
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
new file mode 100644
index 00000000000..1503d14c508
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -0,0 +1,89 @@
+//= require ./board_card
+
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardList = Vue.extend({
+ components: {
+ 'board-card': gl.issueBoards.BoardCard
+ },
+ props: {
+ disabled: Boolean,
+ list: Object,
+ issues: Array,
+ loading: Boolean,
+ issueLinkBase: String
+ },
+ data () {
+ return {
+ scrollOffset: 250,
+ filters: Store.state.filters
+ };
+ },
+ watch: {
+ filters: {
+ handler () {
+ this.list.loadingMore = false;
+ this.$els.list.scrollTop = 0;
+ },
+ deep: true
+ }
+ },
+ methods: {
+ listHeight () {
+ return this.$els.list.getBoundingClientRect().height;
+ },
+ scrollHeight () {
+ return this.$els.list.scrollHeight;
+ },
+ scrollTop () {
+ return this.$els.list.scrollTop + this.listHeight();
+ },
+ loadNextPage () {
+ const getIssues = this.list.nextPage();
+
+ if (getIssues) {
+ this.list.loadingMore = true;
+ getIssues.then(() => {
+ this.list.loadingMore = false;
+ });
+ }
+ },
+ },
+ ready () {
+ const options = gl.issueBoards.getBoardSortableDefaultOptions({
+ group: 'issues',
+ sort: false,
+ disabled: this.disabled,
+ onStart: (e) => {
+ const card = this.$refs.issue[e.oldIndex];
+
+ Store.moving.issue = card.issue;
+ Store.moving.list = card.list;
+ },
+ onAdd: (e) => {
+ gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue);
+ },
+ onRemove: (e) => {
+ this.$refs.issue[e.oldIndex].$destroy(true);
+ }
+ });
+
+ if (bp.getBreakpointSize() === 'xs') {
+ options.handle = '.js-card-drag-handle';
+ }
+
+ this.sortable = Sortable.create(this.$els.list, options);
+
+ // Scroll event on list to load more
+ this.$els.list.onscroll = () => {
+ if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
+ this.loadNextPage();
+ }
+ };
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
new file mode 100644
index 00000000000..1a4d8157970
--- /dev/null
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
@@ -0,0 +1,54 @@
+$(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ $('.js-new-board-list').each(function () {
+ const $this = $(this);
+
+ new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('project-id'));
+
+ $this.glDropdown({
+ data(term, callback) {
+ $.get($this.attr('data-labels'))
+ .then((resp) => {
+ callback(resp);
+ });
+ },
+ renderRow (label) {
+ const active = Store.findList('title', label.title),
+ $li = $('<li />'),
+ $a = $('<a />', {
+ class: (active ? `is-active js-board-list-${active.id}` : ''),
+ text: label.title,
+ href: '#'
+ }),
+ $labelColor = $('<span />', {
+ class: 'dropdown-label-box',
+ style: `background-color: ${label.color}`
+ });
+
+ return $li.append($a.prepend($labelColor));
+ },
+ search: {
+ fields: ['title']
+ },
+ filterable: true,
+ selectable: true,
+ clicked (label, $el, e) {
+ e.preventDefault();
+
+ if (!Store.findList('title', label.title)) {
+ Store.new({
+ title: label.title,
+ position: Store.state.lists.length - 2,
+ list_type: 'label',
+ label: {
+ id: label.id,
+ title: label.title,
+ color: label.color
+ }
+ });
+ }
+ }
+ });
+ });
+});
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
new file mode 100644
index 00000000000..b7afe4897b6
--- /dev/null
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -0,0 +1,25 @@
+((w) => {
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
+ let defaultSortOptions = {
+ forceFallback: true,
+ fallbackClass: 'is-dragging',
+ fallbackOnBody: true,
+ ghostClass: 'is-ghost',
+ filter: '.has-tooltip',
+ scrollSensitivity: 100,
+ scrollSpeed: 20,
+ onStart () {
+ document.body.classList.add('is-dragging');
+ },
+ onEnd () {
+ document.body.classList.remove('is-dragging');
+ }
+ }
+
+ Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
+ return defaultSortOptions;
+ };
+})(window);
diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6
new file mode 100644
index 00000000000..eb082103de9
--- /dev/null
+++ b/app/assets/javascripts/boards/models/issue.js.es6
@@ -0,0 +1,44 @@
+class ListIssue {
+ constructor (obj) {
+ this.id = obj.iid;
+ this.title = obj.title;
+ this.confidential = obj.confidential;
+ this.labels = [];
+
+ if (obj.assignee) {
+ this.assignee = new ListUser(obj.assignee);
+ }
+
+ obj.labels.forEach((label) => {
+ this.labels.push(new ListLabel(label));
+ });
+
+ this.priority = this.labels.reduce((max, label) => {
+ return (label.priority < max) ? label.priority : max;
+ }, Infinity);
+ }
+
+ addLabel (label) {
+ if (!this.findLabel(label)) {
+ this.labels.push(new ListLabel(label));
+ }
+ }
+
+ findLabel (findLabel) {
+ return this.labels.filter( label => label.title === findLabel.title )[0];
+ }
+
+ removeLabel (removeLabel) {
+ if (removeLabel) {
+ this.labels = this.labels.filter( label => removeLabel.title !== label.title );
+ }
+ }
+
+ removeLabels (labels) {
+ labels.forEach(this.removeLabel.bind(this));
+ }
+
+ getLists () {
+ return gl.issueBoards.BoardsStore.state.lists.filter( list => list.findIssue(this.id) );
+ }
+}
diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6
new file mode 100644
index 00000000000..e81e91fe972
--- /dev/null
+++ b/app/assets/javascripts/boards/models/label.js.es6
@@ -0,0 +1,9 @@
+class ListLabel {
+ constructor (obj) {
+ this.id = obj.id;
+ this.title = obj.title;
+ this.color = obj.color;
+ this.description = obj.description;
+ this.priority = (obj.priority !== null) ? obj.priority : Infinity;
+ }
+}
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
new file mode 100644
index 00000000000..be2b8c568a8
--- /dev/null
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -0,0 +1,125 @@
+class List {
+ constructor (obj) {
+ this.id = obj.id;
+ this._uid = this.guid();
+ this.position = obj.position;
+ this.title = obj.title;
+ this.type = obj.list_type;
+ this.preset = ['backlog', 'done', 'blank'].indexOf(this.type) > -1;
+ this.filters = gl.issueBoards.BoardsStore.state.filters;
+ this.page = 1;
+ this.loading = true;
+ this.loadingMore = false;
+ this.issues = [];
+
+ if (obj.label) {
+ this.label = new ListLabel(obj.label);
+ }
+
+ if (this.type !== 'blank' && this.id) {
+ this.getIssues();
+ }
+ }
+
+ guid() {
+ const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
+ return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
+ }
+
+ save () {
+ return gl.boardService.createList(this.label.id)
+ .then((resp) => {
+ const data = resp.json();
+
+ this.id = data.id;
+ this.type = data.list_type;
+ this.position = data.position;
+
+ return this.getIssues();
+ });
+ }
+
+ destroy () {
+ gl.issueBoards.BoardsStore.state.lists.$remove(this);
+ gl.issueBoards.BoardsStore.updateNewListDropdown(this.id);
+
+ gl.boardService.destroyList(this.id);
+ }
+
+ update () {
+ gl.boardService.updateList(this.id, this.position);
+ }
+
+ nextPage () {
+ if (Math.floor(this.issues.length / 20) === this.page) {
+ this.page++;
+
+ return this.getIssues(false);
+ }
+ }
+
+ canSearch () {
+ return this.type === 'backlog';
+ }
+
+ getIssues (emptyIssues = true) {
+ const filters = this.filters;
+ let data = { page: this.page };
+
+ Object.keys(filters).forEach((key) => { data[key] = filters[key]; });
+
+ if (this.label) {
+ data.label_name = data.label_name.filter( label => label !== this.label.title );
+ }
+
+ if (emptyIssues) {
+ this.loading = true;
+ }
+
+ return gl.boardService.getIssuesForList(this.id, data)
+ .then((resp) => {
+ const data = resp.json();
+ this.loading = false;
+
+ if (emptyIssues) {
+ this.issues = [];
+ }
+
+ this.createIssues(data);
+ });
+ }
+
+ createIssues (data) {
+ data.forEach((issueObj) => {
+ this.addIssue(new ListIssue(issueObj));
+ });
+ }
+
+ addIssue (issue, listFrom) {
+ this.issues.push(issue);
+
+ if (this.label) {
+ issue.addLabel(this.label);
+ }
+
+ if (listFrom) {
+ gl.boardService.moveIssue(issue.id, listFrom.id, this.id);
+ }
+ }
+
+ findIssue (id) {
+ return this.issues.filter( issue => issue.id === id )[0];
+ }
+
+ removeIssue (removeIssue) {
+ this.issues = this.issues.filter((issue) => {
+ const matchesRemove = removeIssue.id === issue.id;
+
+ if (matchesRemove) {
+ issue.removeLabel(this.label);
+ }
+
+ return !matchesRemove;
+ });
+ }
+}
diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js.es6
new file mode 100644
index 00000000000..904b3a68507
--- /dev/null
+++ b/app/assets/javascripts/boards/models/user.js.es6
@@ -0,0 +1,8 @@
+class ListUser {
+ constructor (user) {
+ this.id = user.id;
+ this.name = user.name;
+ this.username = user.username;
+ this.avatar = user.avatar_url;
+ }
+}
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
new file mode 100644
index 00000000000..9b80fb2e99f
--- /dev/null
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -0,0 +1,61 @@
+class BoardService {
+ constructor (root) {
+ Vue.http.options.root = root;
+
+ this.lists = Vue.resource(`${root}/lists{/id}`, {}, {
+ generate: {
+ method: 'POST',
+ url: `${root}/lists/generate.json`
+ }
+ });
+ this.issue = Vue.resource(`${root}/issues{/id}`, {});
+ this.issues = Vue.resource(`${root}/lists{/id}/issues`, {});
+
+ Vue.http.interceptors.push((request, next) => {
+ request.headers['X-CSRF-Token'] = $.rails.csrfToken();
+ next();
+ });
+ }
+
+ all () {
+ return this.lists.get();
+ }
+
+ generateDefaultLists () {
+ return this.lists.generate({});
+ }
+
+ createList (label_id) {
+ return this.lists.save({}, {
+ list: {
+ label_id
+ }
+ });
+ }
+
+ updateList (id, position) {
+ return this.lists.update({ id }, {
+ list: {
+ position
+ }
+ });
+ }
+
+ destroyList (id) {
+ return this.lists.delete({ id });
+ }
+
+ getIssuesForList (id, filter = {}) {
+ let data = { id };
+ Object.keys(filter).forEach((key) => { data[key] = filter[key]; });
+
+ return this.issues.get(data);
+ }
+
+ moveIssue (id, from_list_id, to_list_id) {
+ return this.issue.update({ id }, {
+ from_list_id,
+ to_list_id
+ });
+ }
+};
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
new file mode 100644
index 00000000000..18f26a1f911
--- /dev/null
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -0,0 +1,112 @@
+(() => {
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardsStore = {
+ disabled: false,
+ state: {},
+ moving: {
+ issue: {},
+ list: {}
+ },
+ create () {
+ this.state.lists = [];
+ this.state.filters = {
+ author_id: gl.utils.getParameterValues('author_id')[0],
+ assignee_id: gl.utils.getParameterValues('assignee_id')[0],
+ milestone_title: gl.utils.getParameterValues('milestone_title')[0],
+ label_name: gl.utils.getParameterValues('label_name[]')
+ };
+ },
+ addList (listObj) {
+ const list = new List(listObj);
+ this.state.lists.push(list);
+
+ return list;
+ },
+ new (listObj) {
+ const list = this.addList(listObj),
+ backlogList = this.findList('type', 'backlog', 'backlog');
+
+ list
+ .save()
+ .then(() => {
+ // Remove any new issues from the backlog
+ // as they will be visible in the new list
+ list.issues.forEach(backlogList.removeIssue.bind(backlogList));
+ });
+ this.removeBlankState();
+ },
+ updateNewListDropdown (listId) {
+ $(`.js-board-list-${listId}`).removeClass('is-active');
+ },
+ shouldAddBlankState () {
+ // Decide whether to add the blank state
+ return !(this.state.lists.filter( list => list.type !== 'backlog' && list.type !== 'done' )[0]);
+ },
+ addBlankState () {
+ if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
+
+ this.addList({
+ id: 'blank',
+ list_type: 'blank',
+ title: 'Welcome to your Issue Board!',
+ position: 0
+ });
+ },
+ removeBlankState () {
+ this.removeList('blank');
+
+ $.cookie('issue_board_welcome_hidden', 'true', {
+ expires: 365 * 10
+ });
+ },
+ welcomeIsHidden () {
+ return $.cookie('issue_board_welcome_hidden') === 'true';
+ },
+ removeList (id, type = 'blank') {
+ const list = this.findList('id', id, type);
+
+ if (!list) return;
+
+ this.state.lists = this.state.lists.filter( list => list.id !== id );
+ },
+ moveList (listFrom, orderLists) {
+ orderLists.forEach((id, i) => {
+ const list = this.findList('id', parseInt(id));
+
+ list.position = i;
+ });
+ listFrom.update();
+ },
+ moveIssueToList (listFrom, listTo, issue) {
+ const issueTo = listTo.findIssue(issue.id),
+ issueLists = issue.getLists(),
+ listLabels = issueLists.map( listIssue => listIssue.label );
+
+ // Add to new lists issues if it doesn't already exist
+ if (!issueTo) {
+ listTo.addIssue(issue, listFrom);
+ }
+
+ if (listTo.type === 'done' && listFrom.type !== 'backlog') {
+ issueLists.forEach((list) => {
+ list.removeIssue(issue);
+ })
+ issue.removeLabels(listLabels);
+ } else {
+ listFrom.removeIssue(issue);
+ }
+ },
+ findList (key, val, type = 'label') {
+ return this.state.lists.filter((list) => {
+ const byType = type ? list['type'] === type : true;
+
+ return list[key] === val && byType;
+ })[0];
+ },
+ updateFiltersUrl () {
+ history.pushState(null, null, `?${$.param(this.state.filters)}`);
+ }
+ };
+})();
diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/boards/test_utils/simulate_drag.js
new file mode 100755
index 00000000000..75f8b730195
--- /dev/null
+++ b/app/assets/javascripts/boards/test_utils/simulate_drag.js
@@ -0,0 +1,119 @@
+(function () {
+ 'use strict';
+
+ function simulateEvent(el, type, options) {
+ var event;
+ if (!el) return;
+ var ownerDocument = el.ownerDocument;
+
+ options = options || {};
+
+ if (/^mouse/.test(type)) {
+ event = ownerDocument.createEvent('MouseEvents');
+ event.initMouseEvent(type, true, true, ownerDocument.defaultView,
+ options.button, options.screenX, options.screenY, options.clientX, options.clientY,
+ options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
+ } else {
+ event = ownerDocument.createEvent('CustomEvent');
+
+ event.initCustomEvent(type, true, true, ownerDocument.defaultView,
+ options.button, options.screenX, options.screenY, options.clientX, options.clientY,
+ options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
+
+ event.dataTransfer = {
+ data: {},
+
+ setData: function (type, val) {
+ this.data[type] = val;
+ },
+
+ getData: function (type) {
+ return this.data[type];
+ }
+ };
+ }
+
+ if (el.dispatchEvent) {
+ el.dispatchEvent(event);
+ } else if (el.fireEvent) {
+ el.fireEvent('on' + type, event);
+ }
+
+ return event;
+ }
+
+ function getTraget(target) {
+ var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
+ var children = el.children;
+
+ return (
+ children[target.index] ||
+ children[target.index === 'first' ? 0 : -1] ||
+ children[target.index === 'last' ? children.length - 1 : -1]
+ );
+ }
+
+ function getRect(el) {
+ var rect = el.getBoundingClientRect();
+ var width = rect.right - rect.left;
+ var height = rect.bottom - rect.top;
+
+ return {
+ x: rect.left,
+ y: rect.top,
+ cx: rect.left + width / 2,
+ cy: rect.top + height / 2,
+ w: width,
+ h: height,
+ hw: width / 2,
+ wh: height / 2
+ };
+ }
+
+ function simulateDrag(options, callback) {
+ options.to.el = options.to.el || options.from.el;
+
+ var fromEl = getTraget(options.from);
+ var toEl = getTraget(options.to);
+ var scrollable = options.scrollable;
+
+ var fromRect = getRect(fromEl);
+ var toRect = getRect(toEl);
+
+ var startTime = new Date().getTime();
+ var duration = options.duration || 1000;
+ simulateEvent(fromEl, 'mousedown', {button: 0});
+ options.ontap && options.ontap();
+ window.SIMULATE_DRAG_ACTIVE = 1;
+
+ var dragInterval = setInterval(function loop() {
+ var progress = (new Date().getTime() - startTime) / duration;
+ var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft;
+ var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop;
+ var overEl = fromEl.ownerDocument.elementFromPoint(x, y);
+
+ simulateEvent(overEl, 'mousemove', {
+ clientX: x,
+ clientY: y
+ });
+
+ if (progress >= 1) {
+ options.ondragend && options.ondragend();
+ simulateEvent(toEl, 'mouseup');
+ clearInterval(dragInterval);
+ window.SIMULATE_DRAG_ACTIVE = 0;
+ }
+ }, 100);
+
+ return {
+ target: fromEl,
+ fromList: fromEl.parentNode,
+ toList: toEl.parentNode
+ };
+ }
+
+
+ // Export
+ window.simulateEvent = simulateEvent;
+ window.simulateDrag = simulateDrag;
+})();
diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
new file mode 100644
index 00000000000..f9f9f7999d4
--- /dev/null
+++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
@@ -0,0 +1,10 @@
+Vue.http.interceptors.push((request, next) => {
+ Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
+
+ Vue.nextTick(() => {
+ setTimeout(() => {
+ Vue.activeResources--;
+ }, 500);
+ });
+ next();
+});
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 3d9b824d406..0d7d29bb0d0 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -6,19 +6,26 @@
Build.state = null;
- function Build(page_url, build_url, build_status, state1) {
- this.page_url = page_url;
- this.build_url = build_url;
- this.build_status = build_status;
- this.state = state1;
+ function Build(options) {
+ this.page_url = options.page_url;
+ this.build_url = options.build_url;
+ this.build_status = options.build_status;
+ this.state = options.state1;
+ this.build_stage = options.build_stage;
this.hideSidebar = bind(this.hideSidebar, this);
this.toggleSidebar = bind(this.toggleSidebar, this);
+ this.updateDropdown = bind(this.updateDropdown, this);
clearInterval(Build.interval);
this.bp = Breakpoints.get();
- this.hideSidebar();
$('.js-build-sidebar').niceScroll();
+
+ this.populateJobs(this.build_stage);
+ this.updateStageDropdownText(this.build_stage);
+ this.hideSidebar();
+
$(document).off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
$(window).off('resize.build').on('resize.build', this.hideSidebar);
+ $(document).off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
this.updateArtifactRemoveDate();
if ($('#build-trace').length) {
this.getInitialBuildTrace();
@@ -132,6 +139,22 @@
}
};
+ Build.prototype.populateJobs = function(stage) {
+ $('.build-job').hide();
+ $('.build-job[data-stage="' + stage + '"]').show();
+ };
+
+ Build.prototype.updateStageDropdownText = function(stage) {
+ $('.stage-selection').text(stage);
+ };
+
+ Build.prototype.updateDropdown = function(e) {
+ e.preventDefault();
+ var stage = e.currentTarget.text;
+ this.updateStageDropdownText(stage);
+ this.populateJobs(stage);
+ };
+
return Build;
})();
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index c82798cc6a5..c43af17442b 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -34,6 +34,7 @@
$(function() {
var clipboard;
+
clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
clipboard.on('success', genericSuccess);
return clipboard.on('error', genericError);
diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6
new file mode 100644
index 00000000000..46d1c3f00c1
--- /dev/null
+++ b/app/assets/javascripts/create_label.js.es6
@@ -0,0 +1,126 @@
+(function (w) {
+ class CreateLabelDropdown {
+ constructor ($el, projectId) {
+ this.$el = $el;
+ this.projectId = projectId;
+ this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
+ this.$cancelButton = $('.js-cancel-label-btn', this.$el);
+ this.$newLabelField = $('#new_label_name', this.$el);
+ this.$newColorField = $('#new_label_color', this.$el);
+ this.$colorPreview = $('.js-dropdown-label-color-preview', this.$el);
+ this.$newLabelError = $('.js-label-error', this.$el);
+ this.$newLabelCreateButton = $('.js-new-label-btn', this.$el);
+ this.$colorSuggestions = $('.suggest-colors-dropdown a', this.$el);
+
+ this.$newLabelError.hide();
+ this.$newLabelCreateButton.disable();
+
+ this.cleanBinding();
+ this.addBinding();
+ }
+
+ cleanBinding () {
+ this.$colorSuggestions.off('click');
+ this.$newLabelField.off('keyup change');
+ this.$newColorField.off('keyup change');
+ this.$dropdownBack.off('click');
+ this.$cancelButton.off('click');
+ this.$newLabelCreateButton.off('click');
+ }
+
+ addBinding () {
+ const self = this;
+
+ this.$colorSuggestions.on('click', function (e) {
+ const $this = $(this);
+ self.addColorValue(e, $this);
+ });
+
+ this.$newLabelField.on('keyup change', this.enableLabelCreateButton.bind(this));
+ this.$newColorField.on('keyup change', this.enableLabelCreateButton.bind(this));
+
+ this.$dropdownBack.on('click', this.resetForm.bind(this));
+
+ this.$cancelButton.on('click', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ self.resetForm();
+ self.$dropdownBack.trigger('click');
+ });
+
+ this.$newLabelCreateButton.on('click', this.saveLabel.bind(this));
+ }
+
+ addColorValue (e, $this) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.$newColorField.val($this.data('color')).trigger('change');
+ this.$colorPreview
+ .css('background-color', $this.data('color'))
+ .parent()
+ .addClass('is-active');
+ }
+
+ enableLabelCreateButton () {
+ if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') {
+ this.$newLabelError.hide();
+ this.$newLabelCreateButton.enable();
+ } else {
+ this.$newLabelCreateButton.disable();
+ }
+ }
+
+ resetForm () {
+ this.$newLabelField
+ .val('')
+ .trigger('change');
+
+ this.$newColorField
+ .val('')
+ .trigger('change');
+
+ this.$colorPreview
+ .css('background-color', '')
+ .parent()
+ .removeClass('is-active');
+ }
+
+ saveLabel (e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ Api.newLabel(this.projectId, {
+ name: this.$newLabelField.val(),
+ color: this.$newColorField.val()
+ }, (label) => {
+ this.$newLabelCreateButton.enable();
+
+ if (label.message) {
+ let errors;
+
+ if (typeof label.message === 'string') {
+ errors = label.message;
+ } else {
+ errors = label.message.map(function (value, key) {
+ return key + " " + value[0];
+ }).join("<br/>");
+ }
+
+ this.$newLabelError
+ .html(errors)
+ .show();
+ } else {
+ this.$dropdownBack.trigger('click');
+ }
+ });
+ }
+ }
+
+ if (!w.gl) {
+ w.gl = {};
+ }
+
+ gl.CreateLabelDropdown = CreateLabelDropdown;
+})(window);
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 7160fa71ce5..74c4ab563f9 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -88,6 +88,8 @@
new ZenMode();
new MergedButtons();
break;
+ case "projects:merge_requests:conflicts":
+ window.mcui = new MergeConflictResolver()
case 'projects:merge_requests:index':
shortcut_handler = new ShortcutsNavigation();
Issuable.init();
@@ -194,6 +196,9 @@
case 'edit':
new Labels();
}
+ case 'abuse_reports':
+ new gl.AbuseReports();
+ break;
}
break;
case 'dashboard':
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 1bb0b67d0e8..0526430989f 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -4,7 +4,7 @@
var _this;
_this = this;
$('.js-label-select').each(function(i, dropdown) {
- var $block, $colorPreview, $dropdown, $form, $loading, $newLabelCreateButton, $newLabelError, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, newColorField, newLabelField, projectId, resetForm, saveLabel, saveLabelData, selectedLabel, showAny, showNo;
+ var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
labelUrl = $dropdown.data('labels');
@@ -13,8 +13,6 @@
if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) {
selectedLabel = selectedLabel.split(',');
}
- newLabelField = $('#new_label_name');
- newColorField = $('#new_label_color');
showNo = $dropdown.data('show-no');
showAny = $dropdown.data('show-any');
defaultLabel = $dropdown.data('default-label');
@@ -24,10 +22,6 @@
$form = $dropdown.closest('form');
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
$value = $block.find('.value');
- $newLabelError = $('.js-label-error');
- $colorPreview = $('.js-dropdown-label-color-preview');
- $newLabelCreateButton = $('.js-new-label-btn');
- $newLabelError.hide();
$loading = $block.find('.block-loading').fadeOut();
if (issueUpdateURL != null) {
issueURLSplit = issueUpdateURL.split('/');
@@ -36,62 +30,9 @@
labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
labelNoneHTMLTemplate = '<span class="no-value">None</span>';
}
- if (newLabelField.length) {
- $('.suggest-colors-dropdown a').on("click", function(e) {
- e.preventDefault();
- e.stopPropagation();
- newColorField.val($(this).data('color')).trigger('change');
- return $colorPreview.css('background-color', $(this).data('color')).parent().addClass('is-active');
- });
- resetForm = function() {
- newLabelField.val('').trigger('change');
- newColorField.val('').trigger('change');
- return $colorPreview.css('background-color', '').parent().removeClass('is-active');
- };
- $('.dropdown-menu-back').on('click', function() {
- return resetForm();
- });
- $('.js-cancel-label-btn').on('click', function(e) {
- e.preventDefault();
- e.stopPropagation();
- resetForm();
- return $('.dropdown-menu-back', $dropdown.parent()).trigger('click');
- });
- enableLabelCreateButton = function() {
- if (newLabelField.val() !== '' && newColorField.val() !== '') {
- $newLabelError.hide();
- return $newLabelCreateButton.enable();
- } else {
- return $newLabelCreateButton.disable();
- }
- };
- saveLabel = function() {
- return Api.newLabel(projectId, {
- name: newLabelField.val(),
- color: newColorField.val()
- }, function(label) {
- $newLabelCreateButton.enable();
- if (label.message != null) {
- var errorText = label.message;
- if (_.isObject(label.message)) {
- errorText = _.map(label.message, function(value, key) {
- return key + " " + value[0];
- }).join('<br/>');
- }
- return $newLabelError.html(errorText).show();
- } else {
- return $('.dropdown-menu-back', $dropdown.parent()).trigger('click');
- }
- });
- };
- newLabelField.on('keyup change', enableLabelCreateButton);
- newColorField.on('keyup change', enableLabelCreateButton);
- $newLabelCreateButton.disable().on('click', function(e) {
- e.preventDefault();
- e.stopPropagation();
- return saveLabel();
- });
- }
+
+ new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId);
+
saveLabelData = function() {
var data, selected;
selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() {
@@ -272,6 +213,9 @@
isMRIndex = page === 'projects:merge_requests:index';
$selectbox.hide();
$value.removeAttr('style');
+ if (page === 'projects:boards:show') {
+ return;
+ }
if ($dropdown.hasClass('js-multiselect')) {
if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']");
@@ -291,7 +235,7 @@
}
},
multiSelect: $dropdown.hasClass('js-multiselect'),
- clicked: function(label) {
+ clicked: function(label, $el, e) {
var isIssueIndex, isMRIndex, page;
_this.enableBulkLabelDropdown();
if ($dropdown.hasClass('js-filter-bulk-update')) {
@@ -300,7 +244,23 @@
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index';
- if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ if (page === 'projects:boards:show') {
+ if (label.isAny) {
+ gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
+ } else if (label.title) {
+ gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title);
+ } else {
+ var filters = gl.issueBoards.BoardsStore.state.filters['label_name'];
+ filters = filters.filter(function (label) {
+ return label !== $el.text().trim();
+ });
+ gl.issueBoards.BoardsStore.state.filters['label_name'] = filters;
+ }
+
+ gl.issueBoards.BoardsStore.updateFiltersUrl();
+ e.preventDefault();
+ return;
+ } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if (!$dropdown.hasClass('js-multiselect')) {
selectedLabel = label.title;
return Issuable.filterResults($dropdown.closest('form'));
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 130479642f3..b6636de5767 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -104,9 +104,12 @@
return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
});
};
- return gl.text.removeListeners = function(form) {
+ gl.text.removeListeners = function(form) {
return $('.js-md', form).off();
};
+ return gl.text.truncate = function(string, maxLength) {
+ return string.substr(0, (maxLength - 3)) + '...';
+ };
})(window);
}).call(this);
diff --git a/app/assets/javascripts/merge_conflict_data_provider.js.es6 b/app/assets/javascripts/merge_conflict_data_provider.js.es6
new file mode 100644
index 00000000000..cd92df8ddc5
--- /dev/null
+++ b/app/assets/javascripts/merge_conflict_data_provider.js.es6
@@ -0,0 +1,341 @@
+const HEAD_HEADER_TEXT = 'HEAD//our changes';
+const ORIGIN_HEADER_TEXT = 'origin//their changes';
+const HEAD_BUTTON_TITLE = 'Use ours';
+const ORIGIN_BUTTON_TITLE = 'Use theirs';
+
+
+class MergeConflictDataProvider {
+
+ getInitialData() {
+ const diffViewType = $.cookie('diff_view');
+
+ return {
+ isLoading : true,
+ hasError : false,
+ isParallel : diffViewType === 'parallel',
+ diffViewType : diffViewType,
+ isSubmitting : false,
+ conflictsData : {},
+ resolutionData : {}
+ }
+ }
+
+
+ decorateData(vueInstance, data) {
+ this.vueInstance = vueInstance;
+
+ if (data.type === 'error') {
+ vueInstance.hasError = true;
+ data.errorMessage = data.message;
+ }
+ else {
+ data.shortCommitSha = data.commit_sha.slice(0, 7);
+ data.commitMessage = data.commit_message;
+
+ this.setParallelLines(data);
+ this.setInlineLines(data);
+ this.updateResolutionsData(data);
+ }
+
+ vueInstance.conflictsData = data;
+ vueInstance.isSubmitting = false;
+
+ const conflictsText = this.getConflictsCount() > 1 ? 'conflicts' : 'conflict';
+ vueInstance.conflictsData.conflictsText = conflictsText;
+ }
+
+
+ updateResolutionsData(data) {
+ const vi = this.vueInstance;
+
+ data.files.forEach( (file) => {
+ file.sections.forEach( (section) => {
+ if (section.conflict) {
+ vi.$set(`resolutionData['${section.id}']`, false);
+ }
+ });
+ });
+ }
+
+
+ setParallelLines(data) {
+ data.files.forEach( (file) => {
+ file.filePath = this.getFilePath(file);
+ file.iconClass = `fa-${file.blob_icon}`;
+ file.blobPath = file.blob_path;
+ file.parallelLines = [];
+ const linesObj = { left: [], right: [] };
+
+ file.sections.forEach( (section) => {
+ const { conflict, lines, id } = section;
+
+ if (conflict) {
+ linesObj.left.push(this.getOriginHeaderLine(id));
+ linesObj.right.push(this.getHeadHeaderLine(id));
+ }
+
+ lines.forEach( (line) => {
+ const { type } = line;
+
+ if (conflict) {
+ if (type === 'old') {
+ linesObj.left.push(this.getLineForParallelView(line, id, 'conflict'));
+ }
+ else if (type === 'new') {
+ linesObj.right.push(this.getLineForParallelView(line, id, 'conflict', true));
+ }
+ }
+ else {
+ const lineType = type || 'context';
+
+ linesObj.left.push (this.getLineForParallelView(line, id, lineType));
+ linesObj.right.push(this.getLineForParallelView(line, id, lineType, true));
+ }
+ });
+
+ this.checkLineLengths(linesObj);
+ });
+
+ for (let i = 0, len = linesObj.left.length; i < len; i++) {
+ file.parallelLines.push([
+ linesObj.right[i],
+ linesObj.left[i]
+ ]);
+ }
+
+ });
+ }
+
+
+ checkLineLengths(linesObj) {
+ let { left, right } = linesObj;
+
+ if (left.length !== right.length) {
+ if (left.length > right.length) {
+ const diff = left.length - right.length;
+ for (let i = 0; i < diff; i++) {
+ right.push({ lineType: 'emptyLine', richText: '' });
+ }
+ }
+ else {
+ const diff = right.length - left.length;
+ for (let i = 0; i < diff; i++) {
+ left.push({ lineType: 'emptyLine', richText: '' });
+ }
+ }
+ }
+ }
+
+
+ setInlineLines(data) {
+ data.files.forEach( (file) => {
+ file.iconClass = `fa-${file.blob_icon}`;
+ file.blobPath = file.blob_path;
+ file.filePath = this.getFilePath(file);
+ file.inlineLines = []
+
+ file.sections.forEach( (section) => {
+ let currentLineType = 'new';
+ const { conflict, lines, id } = section;
+
+ if (conflict) {
+ file.inlineLines.push(this.getHeadHeaderLine(id));
+ }
+
+ lines.forEach( (line) => {
+ const { type } = line;
+
+ if ((type === 'new' || type === 'old') && currentLineType !== type) {
+ currentLineType = type;
+ file.inlineLines.push({ lineType: 'emptyLine', richText: '' });
+ }
+
+ this.decorateLineForInlineView(line, id, conflict);
+ file.inlineLines.push(line);
+ })
+
+ if (conflict) {
+ file.inlineLines.push(this.getOriginHeaderLine(id));
+ }
+ });
+ });
+ }
+
+
+ handleSelected(sectionId, selection) {
+ const vi = this.vueInstance;
+
+ vi.resolutionData[sectionId] = selection;
+ vi.conflictsData.files.forEach( (file) => {
+ file.inlineLines.forEach( (line) => {
+ if (line.id === sectionId && (line.hasConflict || line.isHeader)) {
+ this.markLine(line, selection);
+ }
+ });
+
+ file.parallelLines.forEach( (lines) => {
+ const left = lines[0];
+ const right = lines[1];
+ const hasSameId = right.id === sectionId || left.id === sectionId;
+ const isLeftMatch = left.hasConflict || left.isHeader;
+ const isRightMatch = right.hasConflict || right.isHeader;
+
+ if (hasSameId && (isLeftMatch || isRightMatch)) {
+ this.markLine(left, selection);
+ this.markLine(right, selection);
+ }
+ })
+ });
+ }
+
+
+ updateViewType(newType) {
+ const vi = this.vueInstance;
+
+ if (newType === vi.diffView || !(newType === 'parallel' || newType === 'inline')) {
+ return;
+ }
+
+ vi.diffView = newType;
+ vi.isParallel = newType === 'parallel';
+ $.cookie('diff_view', newType); // TODO: Make sure that cookie path added.
+ $('.content-wrapper .container-fluid').toggleClass('container-limited');
+ }
+
+
+ markLine(line, selection) {
+ if (selection === 'head' && line.isHead) {
+ line.isSelected = true;
+ line.isUnselected = false;
+ }
+ else if (selection === 'origin' && line.isOrigin) {
+ line.isSelected = true;
+ line.isUnselected = false;
+ }
+ else {
+ line.isSelected = false;
+ line.isUnselected = true;
+ }
+ }
+
+
+ getConflictsCount() {
+ return Object.keys(this.vueInstance.resolutionData).length;
+ }
+
+
+ getResolvedCount() {
+ let count = 0;
+ const data = this.vueInstance.resolutionData;
+
+ for (const id in data) {
+ const resolution = data[id];
+ if (resolution) {
+ count++;
+ }
+ }
+
+ return count;
+ }
+
+
+ isReadyToCommit() {
+ const { conflictsData, isSubmitting } = this.vueInstance
+ const allResolved = this.getConflictsCount() === this.getResolvedCount();
+ const hasCommitMessage = $.trim(conflictsData.commitMessage).length;
+
+ return !isSubmitting && hasCommitMessage && allResolved;
+ }
+
+
+ getCommitButtonText() {
+ const initial = 'Commit conflict resolution';
+ const inProgress = 'Committing...';
+ const vue = this.vueInstance;
+
+ return vue ? vue.isSubmitting ? inProgress : initial : initial;
+ }
+
+
+ decorateLineForInlineView(line, id, conflict) {
+ const { type } = line;
+ line.id = id;
+ line.hasConflict = conflict;
+ line.isHead = type === 'new';
+ line.isOrigin = type === 'old';
+ line.hasMatch = type === 'match';
+ line.richText = line.rich_text;
+ line.isSelected = false;
+ line.isUnselected = false;
+ }
+
+ getLineForParallelView(line, id, lineType, isHead) {
+ const { old_line, new_line, rich_text } = line;
+ const hasConflict = lineType === 'conflict';
+
+ return {
+ id,
+ lineType,
+ hasConflict,
+ isHead : hasConflict && isHead,
+ isOrigin : hasConflict && !isHead,
+ hasMatch : lineType === 'match',
+ lineNumber : isHead ? new_line : old_line,
+ section : isHead ? 'head' : 'origin',
+ richText : rich_text,
+ isSelected : false,
+ isUnselected : false
+ }
+ }
+
+
+ getHeadHeaderLine(id) {
+ return {
+ id : id,
+ richText : HEAD_HEADER_TEXT,
+ buttonTitle : HEAD_BUTTON_TITLE,
+ type : 'new',
+ section : 'head',
+ isHeader : true,
+ isHead : true,
+ isSelected : false,
+ isUnselected: false
+ }
+ }
+
+
+ getOriginHeaderLine(id) {
+ return {
+ id : id,
+ richText : ORIGIN_HEADER_TEXT,
+ buttonTitle : ORIGIN_BUTTON_TITLE,
+ type : 'old',
+ section : 'origin',
+ isHeader : true,
+ isOrigin : true,
+ isSelected : false,
+ isUnselected: false
+ }
+ }
+
+
+ handleFailedRequest(vueInstance, data) {
+ vueInstance.hasError = true;
+ vueInstance.conflictsData.errorMessage = 'Something went wrong!';
+ }
+
+
+ getCommitData() {
+ return {
+ commit_message: this.vueInstance.conflictsData.commitMessage,
+ sections: this.vueInstance.resolutionData
+ }
+ }
+
+
+ getFilePath(file) {
+ const { old_path, new_path } = file;
+ return old_path === new_path ? new_path : `${old_path} → ${new_path}`;
+ }
+
+}
diff --git a/app/assets/javascripts/merge_conflict_resolver.js.es6 b/app/assets/javascripts/merge_conflict_resolver.js.es6
new file mode 100644
index 00000000000..77bffbcb403
--- /dev/null
+++ b/app/assets/javascripts/merge_conflict_resolver.js.es6
@@ -0,0 +1,85 @@
+//= require vue
+
+class MergeConflictResolver {
+
+ constructor() {
+ this.dataProvider = new MergeConflictDataProvider()
+ this.initVue()
+ }
+
+
+ initVue() {
+ const that = this;
+ this.vue = new Vue({
+ el : '#conflicts',
+ name : 'MergeConflictResolver',
+ data : this.dataProvider.getInitialData(),
+ created : this.fetchData(),
+ computed : this.setComputedProperties(),
+ methods : {
+ handleSelected(sectionId, selection) {
+ that.dataProvider.handleSelected(sectionId, selection);
+ },
+ handleViewTypeChange(newType) {
+ that.dataProvider.updateViewType(newType);
+ },
+ commit() {
+ that.commit();
+ }
+ }
+ })
+ }
+
+
+ setComputedProperties() {
+ const dp = this.dataProvider;
+
+ return {
+ conflictsCount() { return dp.getConflictsCount() },
+ resolvedCount() { return dp.getResolvedCount() },
+ readyToCommit() { return dp.isReadyToCommit() },
+ commitButtonText() { return dp.getCommitButtonText() }
+ }
+ }
+
+
+ fetchData() {
+ const dp = this.dataProvider;
+
+ $.get($('#conflicts').data('conflictsPath'))
+ .done((data) => {
+ dp.decorateData(this.vue, data);
+ })
+ .error((data) => {
+ dp.handleFailedRequest(this.vue, data);
+ })
+ .always(() => {
+ this.vue.isLoading = false;
+
+ this.vue.$nextTick(() => {
+ $('#conflicts .js-syntax-highlight').syntaxHighlight();
+ });
+
+ if (this.vue.diffViewType === 'parallel') {
+ $('.content-wrapper .container-fluid').removeClass('container-limited');
+ }
+ })
+ }
+
+
+ commit() {
+ this.vue.isSubmitting = true;
+
+ $.post($('#conflicts').data('resolveConflictsPath'), this.dataProvider.getCommitData())
+ .done((data) => {
+ window.location.href = data.redirect_to;
+ })
+ .error(() => {
+ new Flash('Something went wrong!');
+ })
+ .always(() => {
+ this.vue.isSubmitting = false;
+ });
+ }
+
+}
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 52c2ed61012..4e2273f5aa8 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -9,10 +9,13 @@
MergeRequestTabs.prototype.buildsLoaded = false;
+ MergeRequestTabs.prototype.pipelinesLoaded = false;
+
MergeRequestTabs.prototype.commitsLoaded = false;
function MergeRequestTabs(opts) {
this.opts = opts != null ? opts : {};
+ this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
this.setCurrentAction = bind(this.setCurrentAction, this);
this.tabShown = bind(this.tabShown, this);
this.showTab = bind(this.showTab, this);
@@ -50,10 +53,15 @@
} else if (action === 'builds') {
this.loadBuilds($target.attr('href'));
this.expandView();
+ } else if (action === 'pipelines') {
+ this.loadPipelines($target.attr('href'));
+ this.expandView();
} else {
this.expandView();
}
- return this.setCurrentAction(action);
+ if (this.opts.setUrl) {
+ this.setCurrentAction(action);
+ }
};
MergeRequestTabs.prototype.scrollToElement = function(container) {
@@ -81,7 +89,7 @@
if (action === 'show') {
action = 'notes';
}
- new_state = this._location.pathname.replace(/\/(commits|diffs|builds)(\.html)?\/?$/, '');
+ new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '');
if (action !== 'notes') {
new_state += "/" + action;
}
@@ -177,6 +185,21 @@
});
};
+ MergeRequestTabs.prototype.loadPipelines = function(source) {
+ if (this.pipelinesLoaded) {
+ return;
+ }
+ return this._get({
+ url: source + ".json",
+ success: function(data) {
+ $('#pipelines').html(data.html);
+ gl.utils.localTimeAgo($('.js-timeago', '#pipelines'));
+ this.pipelinesLoaded = true;
+ return this.scrollToElement("#pipelines");
+ }.bind(this)
+ });
+ };
+
MergeRequestTabs.prototype.toggleLoading = function(status) {
return $('.mr-loading-status .loading').toggle(status);
};
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js
index 362aaa906d0..bd35b6f679d 100644
--- a/app/assets/javascripts/merge_request_widget.js
+++ b/app/assets/javascripts/merge_request_widget.js
@@ -28,7 +28,7 @@
MergeRequestWidget.prototype.addEventListeners = function() {
var allowedPages;
- allowedPages = ['show', 'commits', 'builds', 'changes'];
+ allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'];
return $(document).on('page:change.merge_request', (function(_this) {
return function() {
var page;
@@ -53,7 +53,7 @@
return function(data) {
var callback, urlSuffix;
if (data.state === "merged") {
- urlSuffix = deleteSourceBranch ? '?delete_source=true' : '';
+ urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
return window.location.href = window.location.pathname + urlSuffix;
} else if (data.merge_error) {
return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>");
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index a0b65d20c03..e897ebdf630 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -94,7 +94,7 @@
$selectbox.hide();
return $value.css('display', '');
},
- clicked: function(selected) {
+ clicked: function(selected, $el, e) {
var data, isIssueIndex, isMRIndex, page;
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
@@ -102,7 +102,11 @@
if ($dropdown.hasClass('js-filter-bulk-update')) {
return;
}
- if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ if (page === 'projects:boards:show') {
+ gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name;
+ gl.issueBoards.BoardsStore.updateFiltersUrl();
+ e.preventDefault();
+ } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if (selected.name != null) {
selectedMilestone = selected.name;
} else {
diff --git a/app/assets/javascripts/pipeline.js.es6 b/app/assets/javascripts/pipeline.js.es6
new file mode 100644
index 00000000000..bf33eb10100
--- /dev/null
+++ b/app/assets/javascripts/pipeline.js.es6
@@ -0,0 +1,15 @@
+(function() {
+ function toggleGraph() {
+ const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
+ const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
+ const $btnText = $(this).find('.toggle-btn-text');
+
+ $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
+
+ const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
+
+ graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide')
+ }
+
+ $(document).on('click', '.toggle-pipeline-btn', toggleGraph);
+})();
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 65d362e072c..bad82868ab0 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -141,7 +141,7 @@
$selectbox.hide();
return $value.css('display', '');
},
- clicked: function(user) {
+ clicked: function(user, $el, e) {
var isIssueIndex, isMRIndex, page, selected;
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
@@ -149,7 +149,12 @@
if ($dropdown.hasClass('js-filter-bulk-update')) {
return;
}
- if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ if (page === 'projects:boards:show') {
+ selectedId = user.id;
+ gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
+ gl.issueBoards.BoardsStore.updateFiltersUrl();
+ e.preventDefault();
+ } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
selectedId = user.id;
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
diff --git a/app/assets/stylesheets/behaviors.scss b/app/assets/stylesheets/behaviors.scss
index 542a53f0377..a6b9efc49c9 100644
--- a/app/assets/stylesheets/behaviors.scss
+++ b/app/assets/stylesheets/behaviors.scss
@@ -20,3 +20,8 @@
.turn-off { display: block; }
}
}
+
+
+[v-cloak] {
+ display: none;
+}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index f1fe1697d30..6c3786b49bb 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -204,6 +204,10 @@
position: relative;
top: 2px;
}
+
+ svg, .fa {
+ margin-right: 3px;
+ }
}
.btn-lg {
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 5ec5a96a597..d2d60ed7196 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -123,4 +123,9 @@
}
}
}
-} \ No newline at end of file
+}
+
+@mixin dark-diff-match-line {
+ color: rgba(255, 255, 255, 0.3);
+ background: rgba(255, 255, 255, 0.1);
+}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 3fa4a22258d..015fe3debf9 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -222,3 +222,7 @@ header.header-pinned-nav {
padding-right: $sidebar_collapsed_width;
}
}
+
+.right-sidebar {
+ border-left: 1px solid $border-color;
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index ca720022539..5da390118c6 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -276,3 +276,5 @@ $personal-access-tokens-disabled-label-color: #bbb;
$ci-output-bg: #1d1f21;
$ci-text-color: #c5c8c6;
+
+$issue-boards-font-size: 15px;
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index 77a73dc379b..16ffbe57a99 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -21,6 +21,10 @@
// Diff line
.line_holder {
+ &.match .line_content {
+ @include dark-diff-match-line;
+ }
+
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #557;
@@ -36,8 +40,7 @@
}
.line_content.match {
- color: rgba(255, 255, 255, 0.3);
- background: rgba(255, 255, 255, 0.1);
+ @include dark-diff-match-line;
}
}
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index 80a509a7c1a..7de920e074b 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -21,6 +21,10 @@
// Diff line
.line_holder {
+ &.match .line_content {
+ @include dark-diff-match-line;
+ }
+
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #49483e;
@@ -36,8 +40,7 @@
}
.line_content.match {
- color: rgba(255, 255, 255, 0.3);
- background: rgba(255, 255, 255, 0.1);
+ @include dark-diff-match-line;
}
}
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index c62bd021aef..b11499c71ee 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -21,6 +21,10 @@
// Diff line
.line_holder {
+ &.match .line_content {
+ @include dark-diff-match-line;
+ }
+
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #174652;
@@ -36,8 +40,7 @@
}
.line_content.match {
- color: rgba(255, 255, 255, 0.3);
- background: rgba(255, 255, 255, 0.1);
+ @include dark-diff-match-line;
}
}
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index 524cfaf90c3..657bb5e3cd9 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -1,4 +1,10 @@
/* https://gist.github.com/qguv/7936275 */
+
+@mixin matchLine {
+ color: $black-transparent;
+ background: rgba(255, 255, 255, 0.4);
+}
+
.code.solarized-light {
// Line numbers
.line-numbers, .diff-line-num {
@@ -21,6 +27,10 @@
// Diff line
.line_holder {
+ &.match .line_content {
+ @include matchLine;
+ }
+
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #ddd8c5;
@@ -36,8 +46,7 @@
}
.line_content.match {
- color: $black-transparent;
- background: rgba(255, 255, 255, 0.4);
+ @include matchLine;
}
}
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 31a4e3deaac..36a80a916b2 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -1,4 +1,10 @@
/* https://github.com/aahan/pygments-github-style */
+
+@mixin matchLine {
+ color: $black-transparent;
+ background-color: $match-line;
+}
+
.code.white {
// Line numbers
.line-numbers, .diff-line-num {
@@ -22,6 +28,10 @@
// Diff line
.line_holder {
+ &.match .line_content {
+ @include matchLine;
+ }
+
.diff-line-num {
&.old {
background-color: $line-number-old;
@@ -57,8 +67,7 @@
}
&.match {
- color: $black-transparent;
- background-color: $match-line;
+ @include matchLine;
}
&.hll:not(.empty-cell) {
diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss
index 33aedf1f7c1..5bfe9bcb443 100644
--- a/app/assets/stylesheets/mailers/repository_push_email.scss
+++ b/app/assets/stylesheets/mailers/repository_push_email.scss
@@ -45,7 +45,6 @@
.line_content {
padding-left: 0.5em;
padding-right: 0.5em;
- white-space: pre;
&.old {
background-color: $line-removed;
@@ -71,6 +70,10 @@
}
}
+pre {
+ margin: 0;
+}
+
span.highlight_word {
background-color: #fafe3d !important;
}
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index 5607239d92d..c9cdfdcd29c 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -72,7 +72,6 @@
margin-bottom: 20px;
}
-
// Users List
.users-list {
@@ -98,3 +97,44 @@
}
}
}
+
+.abuse-reports {
+ .table {
+ table-layout: fixed;
+ }
+ .subheading {
+ padding-bottom: $gl-padding;
+ }
+ .message {
+ word-wrap: break-word;
+ }
+ .btn {
+ white-space: normal;
+ padding: $gl-btn-padding;
+ }
+ th {
+ width: 15%;
+ &.wide {
+ width: 55%;
+ }
+ }
+ @media (max-width: $screen-sm-max) {
+ th {
+ width: 100%;
+ }
+ td {
+ width: 100%;
+ float: left;
+ }
+ }
+
+ .no-reports {
+ .emoji-icon {
+ margin-left: $btn-side-margin;
+ margin-top: 3px;
+ }
+ span {
+ font-size: 19px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
new file mode 100644
index 00000000000..ad4b2d6496f
--- /dev/null
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -0,0 +1,329 @@
+[v-cloak] {
+ display: none;
+}
+
+.user-can-drag {
+ cursor: -webkit-grab;
+ cursor: grab;
+}
+
+.is-dragging {
+ * {
+ cursor: -webkit-grabbing;
+ cursor: grabbing;
+ }
+}
+
+.dropdown-menu-issues-board-new {
+ width: 320px;
+
+ .dropdown-content {
+ max-height: 150px;
+ }
+}
+
+.issue-board-dropdown-content {
+ margin: 0 8px 10px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid $dropdown-divider-color;
+
+ > p {
+ margin: 0;
+ font-size: 14px;
+ color: #9c9c9c;
+ }
+}
+
+.issue-boards-page {
+ .content-wrapper {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ }
+
+ .sub-nav,
+ .issues-filters {
+ -webkit-flex: none;
+ flex: none;
+ }
+
+ .page-with-sidebar {
+ display: -webkit-flex;
+ display: flex;
+ min-height: 100vh;
+ max-height: 100vh;
+ padding-bottom: 0;
+ }
+
+ .issue-boards-content {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex: 1;
+ flex: 1;
+ width: 100%;
+
+ .content {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ width: 100%;
+ }
+ }
+}
+
+.boards-app-loading {
+ width: 100%;
+ font-size: 34px;
+}
+
+.boards-list {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex: 1;
+ flex: 1;
+ -webkit-flex-basis: 0;
+ flex-basis: 0;
+ min-height: calc(100vh - 152px);
+ max-height: calc(100vh - 152px);
+ padding-top: 25px;
+ padding-right: ($gl-padding / 2);
+ padding-left: ($gl-padding / 2);
+ overflow-x: scroll;
+
+ @media (min-width: $screen-sm-min) {
+ min-height: 475px;
+ max-height: none;
+ }
+}
+
+.board {
+ display: -webkit-flex;
+ display: flex;
+ min-width: calc(100vw - 15px);
+ max-width: calc(100vw - 15px);
+ margin-bottom: 25px;
+ padding-right: ($gl-padding / 2);
+ padding-left: ($gl-padding / 2);
+
+ @media (min-width: $screen-sm-min) {
+ min-width: 400px;
+ max-width: 400px;
+ }
+}
+
+.board-inner {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ width: 100%;
+ font-size: $issue-boards-font-size;
+ background: $background-color;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+}
+
+.board-header {
+ border-top-left-radius: $border-radius-default;
+ border-top-right-radius: $border-radius-default;
+
+ &.has-border {
+ border-top: 3px solid;
+
+ .board-title {
+ padding-top: ($gl-padding - 3px);
+ }
+ }
+}
+
+.board-header-loading-spinner {
+ margin-right: 10px;
+ color: $gray-darkest;
+}
+
+.board-inner-container {
+ border-bottom: 1px solid $border-color;
+ padding: $gl-padding;
+}
+
+.board-title {
+ position: relative;
+ margin: 0;
+ padding: $gl-padding;
+ font-size: 1em;
+ border-bottom: 1px solid $border-color;
+
+ .board-mobile-handle {
+ position: relative;
+ left: 0;
+ top: 1px;
+ margin-top: 0;
+ margin-right: 5px;
+ }
+}
+
+.board-search-container {
+ position: relative;
+ background-color: #fff;
+
+ .form-control {
+ padding-right: 30px;
+ }
+}
+
+.board-search-icon,
+.board-search-clear-btn {
+ position: absolute;
+ right: $gl-padding + 10px;
+ top: 50%;
+ margin-top: -7px;
+ font-size: 14px;
+}
+
+.board-search-icon {
+ color: $gl-placeholder-color;
+}
+
+.board-search-clear-btn {
+ padding: 0;
+ line-height: 1;
+ background: transparent;
+ border: 0;
+ outline: 0;
+
+ &:hover {
+ color: $gl-link-color;
+ }
+}
+
+.board-delete {
+ margin-right: 10px;
+ padding: 0;
+ color: $gray-darkest;
+ background-color: transparent;
+ border: 0;
+ outline: 0;
+
+ &:hover {
+ color: $gl-link-color;
+ }
+}
+
+.board-blank-state {
+ height: 100%;
+ padding: $gl-padding;
+ background-color: #fff;
+}
+
+.board-blank-state-list {
+ list-style: none;
+
+ > li:not(:last-child) {
+ margin-bottom: 8px;
+ }
+
+ .label-color {
+ position: relative;
+ top: 2px;
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ margin-right: 3px;
+ border-radius: $border-radius-default;
+ }
+}
+
+.board-list {
+ -webkit-flex: 1;
+ flex: 1;
+ height: 400px;
+ margin-bottom: 0;
+ padding: 5px;
+ overflow-y: scroll;
+ overflow-x: hidden;
+}
+
+.board-list-loading {
+ margin-top: 10px;
+ font-size: 26px;
+}
+
+.is-ghost {
+ opacity: 0.3;
+}
+
+.is-dragging {
+ // Important because plugin sets inline CSS
+ opacity: 1!important;
+}
+
+.card {
+ position: relative;
+ width: 100%;
+ padding: 10px $gl-padding;
+ background: #fff;
+ border-radius: $border-radius-default;
+ box-shadow: 0 1px 2px rgba(186, 186, 186, 0.5);
+ list-style: none;
+
+ &.user-can-drag {
+ padding-left: ($gl-padding * 2);
+
+ @media (min-width: $screen-sm-min) {
+ padding-left: $gl-padding;
+ }
+ }
+
+ &:not(:last-child) {
+ margin-bottom: 5px;
+ }
+
+ a {
+ cursor: pointer;
+ }
+
+ .label {
+ border: 0;
+ outline: 0;
+ }
+
+ .confidential-icon {
+ margin-right: 5px;
+ }
+}
+
+.board-mobile-handle {
+ position: absolute;
+ left: 10px;
+ top: 50%;
+ margin-top: (-15px / 2);
+
+ @media (min-width: $screen-sm-min) {
+ display: none;
+ }
+}
+
+.card-title {
+ margin: 0;
+ font-size: 1em;
+
+ a {
+ color: inherit;
+ }
+}
+
+.card-footer {
+ margin-top: 5px;
+
+ .label {
+ margin-right: 4px;
+ font-size: (14px / $issue-boards-font-size) * 1em;
+ }
+}
+
+.card-number {
+ margin-right: 8px;
+ font-weight: 500;
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index e26f8f7080d..81fce55853c 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -53,14 +53,6 @@
left: 70px;
}
}
-
- .nav-links {
- svg {
- position: relative;
- top: 2px;
- margin-right: 3px;
- }
- }
}
.build-header {
@@ -108,24 +100,98 @@
}
.right-sidebar.build-sidebar {
- padding-top: $gl-padding;
- padding-bottom: $gl-padding;
+ padding: $gl-padding 0;
&.right-sidebar-collapsed {
display: none;
}
+ .blocks-container {
+ padding: $gl-padding;
+ }
+
.block {
width: 100%;
}
.build-sidebar-header {
- padding-top: 0;
+ padding: 0 $gl-padding $gl-padding;
.gutter-toggle {
margin-top: 0;
}
}
+
+ .stage-item {
+ cursor: pointer;
+
+ &:hover {
+ color: $gl-text-color;
+ }
+ }
+
+ .build-dropdown {
+ padding: 0 $gl-padding;
+
+ .dropdown-menu-toggle {
+ margin-top: 8px;
+ }
+
+ .dropdown-menu {
+ right: $gl-padding;
+ left: $gl-padding;
+ width: auto;
+ }
+ }
+
+ .builds-container {
+ margin-top: $gl-padding;
+ background-color: $white-light;
+ border-top: 1px solid $border-color;
+ border-bottom: 1px solid $border-color;
+ max-height: 300px;
+ overflow: scroll;
+
+ svg {
+ position: relative;
+ top: 2px;
+ margin-right: 3px;
+ height: 13px;
+ }
+
+ a {
+ display: block;
+ padding: $gl-padding 10px $gl-padding 40px;
+ width: 270px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &:hover {
+ background-color: $row-hover;
+ color: $gl-text-color;
+ }
+ }
+
+ .build-job {
+ position: relative;
+
+ .fa {
+ position: absolute;
+ left: 15px;
+ top: 20px;
+ display: none;
+ }
+
+ &.active {
+ font-weight: bold;
+
+ .fa {
+ display: block;
+ }
+ }
+ }
+ }
}
.build-detail-row {
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
new file mode 100644
index 00000000000..1f499897c16
--- /dev/null
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -0,0 +1,238 @@
+$colors: (
+ white_header_head_neutral : #e1fad7,
+ white_line_head_neutral : #effdec,
+ white_button_head_neutral : #9adb84,
+
+ white_header_head_chosen : #baf0a8,
+ white_line_head_chosen : #e1fad7,
+ white_button_head_chosen : #52c22d,
+
+ white_header_origin_neutral : #e0f0ff,
+ white_line_origin_neutral : #f2f9ff,
+ white_button_origin_neutral : #87c2fa,
+
+ white_header_origin_chosen : #add8ff,
+ white_line_origin_chosen : #e0f0ff,
+ white_button_origin_chosen : #268ced,
+
+ white_header_not_chosen : #f0f0f0,
+ white_line_not_chosen : #f9f9f9,
+
+
+ dark_header_head_neutral : rgba(#3f3, .2),
+ dark_line_head_neutral : rgba(#3f3, .1),
+ dark_button_head_neutral : #40874f,
+
+ dark_header_head_chosen : rgba(#3f3, .33),
+ dark_line_head_chosen : rgba(#3f3, .2),
+ dark_button_head_chosen : #258537,
+
+ dark_header_origin_neutral : rgba(#2878c9, .4),
+ dark_line_origin_neutral : rgba(#2878c9, .3),
+ dark_button_origin_neutral : #2a5c8c,
+
+ dark_header_origin_chosen : rgba(#2878c9, .6),
+ dark_line_origin_chosen : rgba(#2878c9, .4),
+ dark_button_origin_chosen : #1d6cbf,
+
+ dark_header_not_chosen : rgba(#fff, .25),
+ dark_line_not_chosen : rgba(#fff, .1),
+
+
+ monokai_header_head_neutral : rgba(#a6e22e, .25),
+ monokai_line_head_neutral : rgba(#a6e22e, .1),
+ monokai_button_head_neutral : #376b20,
+
+ monokai_header_head_chosen : rgba(#a6e22e, .4),
+ monokai_line_head_chosen : rgba(#a6e22e, .25),
+ monokai_button_head_chosen : #39800d,
+
+ monokai_header_origin_neutral : rgba(#60d9f1, .35),
+ monokai_line_origin_neutral : rgba(#60d9f1, .15),
+ monokai_button_origin_neutral : #38848c,
+
+ monokai_header_origin_chosen : rgba(#60d9f1, .5),
+ monokai_line_origin_chosen : rgba(#60d9f1, .35),
+ monokai_button_origin_chosen : #3ea4b2,
+
+ monokai_header_not_chosen : rgba(#76715d, .24),
+ monokai_line_not_chosen : rgba(#76715d, .1),
+
+
+ solarized_light_header_head_neutral : rgba(#859900, .37),
+ solarized_light_line_head_neutral : rgba(#859900, .2),
+ solarized_light_button_head_neutral : #afb262,
+
+ solarized_light_header_head_chosen : rgba(#859900, .5),
+ solarized_light_line_head_chosen : rgba(#859900, .37),
+ solarized_light_button_head_chosen : #94993d,
+
+ solarized_light_header_origin_neutral : rgba(#2878c9, .37),
+ solarized_light_line_origin_neutral : rgba(#2878c9, .15),
+ solarized_light_button_origin_neutral : #60a1bf,
+
+ solarized_light_header_origin_chosen : rgba(#2878c9, .6),
+ solarized_light_line_origin_chosen : rgba(#2878c9, .37),
+ solarized_light_button_origin_chosen : #2482b2,
+
+ solarized_light_header_not_chosen : rgba(#839496, .37),
+ solarized_light_line_not_chosen : rgba(#839496, .2),
+
+
+ solarized_dark_header_head_neutral : rgba(#859900, .35),
+ solarized_dark_line_head_neutral : rgba(#859900, .15),
+ solarized_dark_button_head_neutral : #376b20,
+
+ solarized_dark_header_head_chosen : rgba(#859900, .5),
+ solarized_dark_line_head_chosen : rgba(#859900, .35),
+ solarized_dark_button_head_chosen : #39800d,
+
+ solarized_dark_header_origin_neutral : rgba(#2878c9, .35),
+ solarized_dark_line_origin_neutral : rgba(#2878c9, .15),
+ solarized_dark_button_origin_neutral : #086799,
+
+ solarized_dark_header_origin_chosen : rgba(#2878c9, .6),
+ solarized_dark_line_origin_chosen : rgba(#2878c9, .35),
+ solarized_dark_button_origin_chosen : #0082cc,
+
+ solarized_dark_header_not_chosen : rgba(#839496, .25),
+ solarized_dark_line_not_chosen : rgba(#839496, .15)
+);
+
+
+@mixin color-scheme($color) {
+ .header.line_content, .diff-line-num {
+ &.origin {
+ background-color: map-get($colors, #{$color}_header_origin_neutral);
+ border-color: map-get($colors, #{$color}_header_origin_neutral);
+
+ button {
+ background-color: map-get($colors, #{$color}_button_origin_neutral);
+ border-color: darken(map-get($colors, #{$color}_button_origin_neutral), 15);
+ }
+
+ &.selected {
+ background-color: map-get($colors, #{$color}_header_origin_chosen);
+ border-color: map-get($colors, #{$color}_header_origin_chosen);
+
+ button {
+ background-color: map-get($colors, #{$color}_button_origin_chosen);
+ border-color: darken(map-get($colors, #{$color}_button_origin_chosen), 15);
+ }
+ }
+
+ &.unselected {
+ background-color: map-get($colors, #{$color}_header_not_chosen);
+ border-color: map-get($colors, #{$color}_header_not_chosen);
+
+ button {
+ background-color: lighten(map-get($colors, #{$color}_button_origin_neutral), 15);
+ border-color: map-get($colors, #{$color}_button_origin_neutral);
+ }
+ }
+ }
+ &.head {
+ background-color: map-get($colors, #{$color}_header_head_neutral);
+ border-color: map-get($colors, #{$color}_header_head_neutral);
+
+ button {
+ background-color: map-get($colors, #{$color}_button_head_neutral);
+ border-color: darken(map-get($colors, #{$color}_button_head_neutral), 15);
+ }
+
+ &.selected {
+ background-color: map-get($colors, #{$color}_header_head_chosen);
+ border-color: map-get($colors, #{$color}_header_head_chosen);
+
+ button {
+ background-color: map-get($colors, #{$color}_button_head_chosen);
+ border-color: darken(map-get($colors, #{$color}_button_head_chosen), 15);
+ }
+ }
+
+ &.unselected {
+ background-color: map-get($colors, #{$color}_header_not_chosen);
+ border-color: map-get($colors, #{$color}_header_not_chosen);
+
+ button {
+ background-color: lighten(map-get($colors, #{$color}_button_head_neutral), 15);
+ border-color: map-get($colors, #{$color}_button_head_neutral);
+ }
+ }
+ }
+ }
+
+ .line_content {
+ &.origin {
+ background-color: map-get($colors, #{$color}_line_origin_neutral);
+
+ &.selected {
+ background-color: map-get($colors, #{$color}_line_origin_chosen);
+ }
+
+ &.unselected {
+ background-color: map-get($colors, #{$color}_line_not_chosen);
+ }
+ }
+ &.head {
+ background-color: map-get($colors, #{$color}_line_head_neutral);
+
+ &.selected {
+ background-color: map-get($colors, #{$color}_line_head_chosen);
+ }
+
+ &.unselected {
+ background-color: map-get($colors, #{$color}_line_not_chosen);
+ }
+ }
+ }
+}
+
+
+#conflicts {
+
+ .white {
+ @include color-scheme('white')
+ }
+
+ .dark {
+ @include color-scheme('dark')
+ }
+
+ .monokai {
+ @include color-scheme('monokai')
+ }
+
+ .solarized-light {
+ @include color-scheme('solarized_light')
+ }
+
+ .solarized-dark {
+ @include color-scheme('solarized_dark')
+ }
+
+ .diff-wrap-lines .line_content {
+ white-space: normal;
+ min-height: 19px;
+ }
+
+ .line_content.header {
+ position: relative;
+
+ button {
+ border-radius: 2px;
+ font-size: 10px;
+ position: absolute;
+ right: 10px;
+ padding: 0;
+ outline: none;
+ color: #fff;
+ width: 75px; // static width to make 2 buttons have same width
+ height: 19px;
+ }
+ }
+
+ .btn-success .fa-spinner {
+ color: #fff;
+ }
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 21919fe4d73..ce1c424624f 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -229,3 +229,196 @@
box-shadow: none;
}
}
+
+// Pipeline visualization
+
+.toggle-pipeline-btn {
+ background-color: $gray-dark;
+
+ .caret {
+ border-top: none;
+ border-bottom: 4px solid;
+ }
+
+ &.graph-collapsed {
+ background-color: $white-light;
+
+ .caret {
+ border-bottom: none;
+ border-top: 4px solid;
+ }
+ }
+}
+
+.pipeline-graph {
+ width: 100%;
+ overflow: auto;
+ white-space: nowrap;
+ max-height: 500px;
+ transition: max-height 0.3s, padding 0.3s;
+
+ &.graph-collapsed {
+ max-height: 0;
+ padding: 0 16px;
+ }
+}
+
+.pipeline-visualization {
+ position: relative;
+ min-width: 1220px;
+
+ ul {
+ padding: 0;
+ }
+}
+
+.stage-column {
+ display: inline-block;
+ vertical-align: top;
+ margin-right: 50px;
+
+ li {
+ list-style: none;
+ }
+
+ .stage-name {
+ margin-bottom: 15px;
+ font-weight: bold;
+ width: 150px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .build {
+ border: 1px solid $border-color;
+ position: relative;
+ padding: 6px 10px;
+ border-radius: 30px;
+ width: 150px;
+ margin-bottom: 10px;
+
+ &.playable {
+ background-color: $gray-light;
+ }
+
+ .build-content {
+ width: 130px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ a {
+ color: $layout-link-gray;
+ }
+ }
+
+ svg {
+ position: relative;
+ top: 2px;
+ margin-right: 5px;
+ }
+
+ .fa {
+ font-size: 13px;
+ }
+
+ // Connect first build in each stage with right horizontal line
+ &:first-child {
+ &::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ right: -54px;
+ border-top: 2px solid $border-color;
+ width: 54px;
+ height: 1px;
+ }
+ }
+
+ // Connect each build (except for first) with curved lines
+ &:not(:first-child) {
+ &::after, &::before {
+ content: '';
+ top: -47px;
+ position: absolute;
+ border-bottom: 2px solid $border-color;
+ width: 20px;
+ height: 65px;
+ }
+
+ // Right connecting curves
+ &::after {
+ right: -20px;
+ border-right: 2px solid $border-color;
+ border-radius: 0 0 50px;
+ }
+
+ // Left connecting curves
+ &::before {
+ left: -20px;
+ border-left: 2px solid $border-color;
+ border-radius: 0 0 0 50px;
+ }
+ }
+
+ // Connect second build to first build with smaller curved line
+ &:nth-child(2) {
+ &::after, &::before {
+ height: 45px;
+ top: -26px;
+ }
+ }
+ }
+
+ &:last-child {
+ .build {
+ // Remove right connecting horizontal line from first build in last stage
+ &:first-child {
+ &::after, &::before {
+ border: none;
+ }
+ }
+ // Remove right curved connectors from all builds in last stage
+ &:not(:first-child) {
+ &::after {
+ border: none;
+ }
+ }
+ }
+ }
+
+ &:first-child {
+ .build {
+ // Remove left curved connectors from all builds in first stage
+ &:not(:first-child) {
+ &::before {
+ border: none;
+ }
+ }
+ }
+ }
+}
+
+.pipeline-actions {
+ border-bottom: none;
+}
+
+.toggle-pipeline-btn {
+
+ .fa {
+ color: $dropdown-header-color;
+ }
+}
+
+.pipelines.tab-pane {
+
+ .content-list.pipelines {
+ overflow: scroll;
+ }
+
+ .stage {
+ max-width: 60px;
+ width: 60px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 46371ec6871..6f58203f49c 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -228,3 +228,9 @@
}
}
}
+
+table.u2f-registrations {
+ th:not(:last-child), td:not(:last-child) {
+ border-right: solid 1px transparent;
+ }
+} \ No newline at end of file
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 1243bb96d4d..c8390af3b36 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -6,7 +6,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end
def destroy
- TodoService.new.mark_todos_as_done([todo], current_user)
+ TodoService.new.mark_todos_as_done_by_ids([params[:id]], current_user)
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
@@ -27,10 +27,6 @@ class Dashboard::TodosController < Dashboard::ApplicationController
private
- def todo
- @todo ||= find_todos.find(params[:id])
- end
-
def find_todos
@todos ||= TodosFinder.new(current_user, params).execute
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index e37e9e136db..9eb75bb3891 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -43,11 +43,11 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
# A U2F (universal 2nd factor) device's information is stored after successful
# registration, which is then used while 2FA authentication is taking place.
def create_u2f
- @u2f_registration = U2fRegistration.register(current_user, u2f_app_id, params[:device_response], session[:challenges])
+ @u2f_registration = U2fRegistration.register(current_user, u2f_app_id, u2f_registration_params, session[:challenges])
if @u2f_registration.persisted?
session.delete(:challenges)
- redirect_to profile_account_path, notice: "Your U2F device was registered!"
+ redirect_to profile_two_factor_auth_path, notice: "Your U2F device was registered!"
else
@qr_code = build_qr_code
setup_u2f_registration
@@ -91,15 +91,19 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
# Actual communication is performed using a Javascript API
def setup_u2f_registration
@u2f_registration ||= U2fRegistration.new
- @registration_key_handles = current_user.u2f_registrations.pluck(:key_handle)
+ @u2f_registrations = current_user.u2f_registrations
u2f = U2F::U2F.new(u2f_app_id)
registration_requests = u2f.registration_requests
- sign_requests = u2f.authentication_requests(@registration_key_handles)
+ sign_requests = u2f.authentication_requests(@u2f_registrations.map(&:key_handle))
session[:challenges] = registration_requests.map(&:challenge)
gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id,
register_requests: registration_requests,
sign_requests: sign_requests })
end
+
+ def u2f_registration_params
+ params.require(:u2f_registration).permit(:device_response, :name)
+ end
end
diff --git a/app/controllers/profiles/u2f_registrations_controller.rb b/app/controllers/profiles/u2f_registrations_controller.rb
new file mode 100644
index 00000000000..c02fe85c3cc
--- /dev/null
+++ b/app/controllers/profiles/u2f_registrations_controller.rb
@@ -0,0 +1,7 @@
+class Profiles::U2fRegistrationsController < Profiles::ApplicationController
+ def destroy
+ u2f_registration = current_user.u2f_registrations.find(params[:id])
+ u2f_registration.destroy
+ redirect_to profile_two_factor_auth_path, notice: "Successfully deleted U2F device."
+ end
+end
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 996909a28c6..91315a07deb 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -83,6 +83,7 @@ class Projects::ApplicationController < ApplicationController
end
def apply_diff_view_cookie!
+ @show_changes_tab = params[:view].present?
cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present?
end
diff --git a/app/controllers/projects/board_lists_controller.rb b/app/controllers/projects/board_lists_controller.rb
new file mode 100644
index 00000000000..3cfb08d5822
--- /dev/null
+++ b/app/controllers/projects/board_lists_controller.rb
@@ -0,0 +1,65 @@
+class Projects::BoardListsController < Projects::ApplicationController
+ respond_to :json
+
+ before_action :authorize_admin_list!
+
+ rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
+
+ def create
+ list = Boards::Lists::CreateService.new(project, current_user, list_params).execute
+
+ if list.valid?
+ render json: list.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } })
+ else
+ render json: list.errors, status: :unprocessable_entity
+ end
+ end
+
+ def update
+ service = Boards::Lists::MoveService.new(project, current_user, move_params)
+
+ if service.execute
+ head :ok
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ def destroy
+ service = Boards::Lists::DestroyService.new(project, current_user, params)
+
+ if service.execute
+ head :ok
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ def generate
+ service = Boards::Lists::GenerateService.new(project, current_user)
+
+ if service.execute
+ render json: project.board.lists.label.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } })
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ private
+
+ def authorize_admin_list!
+ return render_403 unless can?(current_user, :admin_list, project)
+ end
+
+ def list_params
+ params.require(:list).permit(:label_id)
+ end
+
+ def move_params
+ params.require(:list).permit(:position).merge(id: params[:id])
+ end
+
+ def record_not_found(exception)
+ render json: { error: exception.message }, status: :not_found
+ end
+end
diff --git a/app/controllers/projects/boards/application_controller.rb b/app/controllers/projects/boards/application_controller.rb
new file mode 100644
index 00000000000..dad38fff6b9
--- /dev/null
+++ b/app/controllers/projects/boards/application_controller.rb
@@ -0,0 +1,15 @@
+module Projects
+ module Boards
+ class ApplicationController < Projects::ApplicationController
+ respond_to :json
+
+ rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
+
+ private
+
+ def record_not_found(exception)
+ render json: { error: exception.message }, status: :not_found
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
new file mode 100644
index 00000000000..2d894b3dd4a
--- /dev/null
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -0,0 +1,56 @@
+module Projects
+ module Boards
+ class IssuesController < Boards::ApplicationController
+ before_action :authorize_read_issue!, only: [:index]
+ before_action :authorize_update_issue!, only: [:update]
+
+ def index
+ issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
+ issues = issues.page(params[:page])
+
+ render json: issues.as_json(
+ only: [:iid, :title, :confidential],
+ include: {
+ assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
+ labels: { only: [:id, :title, :description, :color, :priority] }
+ })
+ end
+
+ def update
+ service = ::Boards::Issues::MoveService.new(project, current_user, move_params)
+
+ if service.execute(issue)
+ head :ok
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ private
+
+ def issue
+ @issue ||=
+ IssuesFinder.new(current_user, project_id: project.id, state: 'all')
+ .execute
+ .where(iid: params[:id])
+ .first!
+ end
+
+ def authorize_read_issue!
+ return render_403 unless can?(current_user, :read_issue, project)
+ end
+
+ def authorize_update_issue!
+ return render_403 unless can?(current_user, :update_issue, issue)
+ end
+
+ def filter_params
+ params.merge(id: params[:list_id])
+ end
+
+ def move_params
+ params.permit(:id, :from_list_id, :to_list_id)
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/boards/lists_controller.rb b/app/controllers/projects/boards/lists_controller.rb
new file mode 100644
index 00000000000..b995f586737
--- /dev/null
+++ b/app/controllers/projects/boards/lists_controller.rb
@@ -0,0 +1,81 @@
+module Projects
+ module Boards
+ class ListsController < Boards::ApplicationController
+ before_action :authorize_admin_list!, only: [:create, :update, :destroy, :generate]
+ before_action :authorize_read_list!, only: [:index]
+
+ def index
+ render json: serialize_as_json(project.board.lists)
+ end
+
+ def create
+ list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute
+
+ if list.valid?
+ render json: serialize_as_json(list)
+ else
+ render json: list.errors, status: :unprocessable_entity
+ end
+ end
+
+ def update
+ list = project.board.lists.movable.find(params[:id])
+ service = ::Boards::Lists::MoveService.new(project, current_user, move_params)
+
+ if service.execute(list)
+ head :ok
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ def destroy
+ list = project.board.lists.destroyable.find(params[:id])
+ service = ::Boards::Lists::DestroyService.new(project, current_user, params)
+
+ if service.execute(list)
+ head :ok
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ def generate
+ service = ::Boards::Lists::GenerateService.new(project, current_user)
+
+ if service.execute
+ render json: serialize_as_json(project.board.lists.movable)
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ private
+
+ def authorize_admin_list!
+ return render_403 unless can?(current_user, :admin_list, project)
+ end
+
+ def authorize_read_list!
+ return render_403 unless can?(current_user, :read_list, project)
+ end
+
+ def list_params
+ params.require(:list).permit(:label_id)
+ end
+
+ def move_params
+ params.require(:list).permit(:position)
+ end
+
+ def serialize_as_json(resource)
+ resource.as_json(
+ only: [:id, :list_type, :position],
+ methods: [:title],
+ include: {
+ label: { only: [:id, :title, :description, :color, :priority] }
+ })
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
new file mode 100644
index 00000000000..33206717089
--- /dev/null
+++ b/app/controllers/projects/boards_controller.rb
@@ -0,0 +1,15 @@
+class Projects::BoardsController < Projects::ApplicationController
+ respond_to :html
+
+ before_action :authorize_read_board!, only: [:show]
+
+ def show
+ ::Boards::CreateService.new(project, current_user).execute
+ end
+
+ private
+
+ def authorize_read_board!
+ return access_denied! unless can?(current_user, :read_board, project)
+ end
+end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 139680d2df9..4c1f38be9f3 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -9,15 +9,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled
before_action :merge_request, only: [
- :edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check,
- :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip
+ :edit, :update, :show, :diffs, :commits, :conflicts, :builds, :pipelines, :merge, :merge_check,
+ :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts
]
- before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
- before_action :define_show_vars, only: [:show, :diffs, :commits, :builds]
+ before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines]
+ before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :builds, :pipelines]
before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
before_action :define_commit_vars, only: [:diffs]
before_action :define_diff_comment_vars, only: [:diffs]
- before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds]
+ before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines]
# Allow read any merge_request
before_action :authorize_read_merge_request!
@@ -28,6 +28,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
# Allow modify merge_request
before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort]
+ before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts]
+
def index
terms = params['issue_search']
@merge_requests = merge_requests_collection
@@ -130,6 +132,47 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
+ def conflicts
+ respond_to do |format|
+ format.html { define_discussion_vars }
+
+ format.json do
+ if @merge_request.conflicts_can_be_resolved_in_ui?
+ render json: @merge_request.conflicts
+ elsif @merge_request.can_be_merged?
+ render json: {
+ message: 'The merge conflicts for this merge request have already been resolved. Please return to the merge request.',
+ type: 'error'
+ }
+ else
+ render json: {
+ message: 'The merge conflicts for this merge request cannot be resolved through GitLab. Please try to resolve them locally.',
+ type: 'error'
+ }
+ end
+ end
+ end
+ end
+
+ def resolve_conflicts
+ return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui?
+
+ if @merge_request.can_be_merged?
+ render status: :bad_request, json: { message: 'The merge conflicts for this merge request have already been resolved.' }
+ return
+ end
+
+ begin
+ MergeRequests::ResolveService.new(@merge_request.source_project, current_user, params).execute(@merge_request)
+
+ flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.'
+
+ render json: { redirect_to: namespace_project_merge_request_url(@project.namespace, @project, @merge_request, resolved_conflicts: true) }
+ rescue Gitlab::Conflict::File::MissingResolution => e
+ render status: :bad_request, json: { message: e.message }
+ end
+ end
+
def builds
respond_to do |format|
format.html do
@@ -141,7 +184,22 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
+ def pipelines
+ @pipelines = @merge_request.all_pipelines
+
+ respond_to do |format|
+ format.html do
+ define_discussion_vars
+
+ render 'show'
+ end
+ format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_pipelines') } }
+ end
+ end
+
def new
+ apply_diff_view_cookie!
+
build_merge_request
@noteable = @merge_request
@@ -158,7 +216,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@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
@@ -338,6 +395,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
return render_404 unless can?(current_user, :admin_merge_request, @merge_request)
end
+ def authorize_can_resolve_conflicts!
+ return render_404 unless @merge_request.conflicts_can_be_resolved_by?(current_user)
+ end
+
def module_enabled
return render_404 unless @project.merge_requests_enabled
end
@@ -412,7 +473,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
noteable_id: @merge_request.id
}
- @use_legacy_diff_notes = !@merge_request.support_new_diff_notes?
+ @use_legacy_diff_notes = !@merge_request.has_complete_diff_refs?
@grouped_diff_discussions = @merge_request.notes.inc_author_project_award_emoji.grouped_diff_discussions
Banzai::NoteRenderer.render(
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index c3613bc67dd..f3733b01721 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -320,4 +320,8 @@ module ApplicationHelper
capture(&block)
end
end
+
+ def page_class
+ "issue-boards-page" if current_controller?(:boards)
+ end
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 1cb5d847626..9ea03720c1e 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -11,17 +11,14 @@ module BlobHelper
def edit_blob_link(project = @project, ref = @ref, path = @path, options = {})
return unless current_user
- blob = project.repository.blob_at(ref, path) rescue nil
+ blob = options.delete(:blob)
+ blob ||= project.repository.blob_at(ref, path) rescue nil
return unless blob
- from_mr = options[:from_merge_request_id]
- link_opts = {}
- link_opts[:from_merge_request_id] = from_mr if from_mr
-
edit_path = namespace_project_edit_blob_path(project.namespace, project,
tree_join(ref, path),
- link_opts)
+ options[:link_opts])
if !on_top_of_branch?(project, ref)
button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' }
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index ea2f5f9281a..94df7d131ca 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -38,6 +38,10 @@ module CiStatusHelper
'icon_status_pending'
when 'running'
'icon_status_running'
+ when 'play'
+ return icon('play fw')
+ when 'created'
+ 'icon_status_pending'
else
'icon_status_cancel'
end
@@ -48,13 +52,13 @@ module CiStatusHelper
def render_commit_status(commit, tooltip_placement: 'auto left')
project = commit.project
path = builds_namespace_project_commit_path(project.namespace, project, commit)
- render_status_with_link('commit', commit.status, path, tooltip_placement)
+ render_status_with_link('commit', commit.status, path, tooltip_placement: tooltip_placement)
end
def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
project = pipeline.project
path = namespace_project_pipeline_path(project.namespace, project, pipeline)
- render_status_with_link('pipeline', pipeline.status, path, tooltip_placement)
+ render_status_with_link('pipeline', pipeline.status, path, tooltip_placement: tooltip_placement)
end
def no_runners_for_project?(project)
@@ -62,13 +66,17 @@ module CiStatusHelper
Ci::Runner.shared.blank?
end
- private
+ def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '')
+ klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}"
+ title = "#{type.titleize}: #{ci_label_for_status(status)}"
+ data = { toggle: 'tooltip', placement: tooltip_placement }
- def render_status_with_link(type, status, path, tooltip_placement, cssclass: '')
- link_to ci_icon_for_status(status),
- path,
- class: "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}",
- title: "#{type.titleize}: #{ci_label_for_status(status)}",
- data: { toggle: 'tooltip', placement: tooltip_placement }
+ if path
+ link_to ci_icon_for_status(status), path,
+ class: klass, title: title, data: data
+ else
+ content_tag :span, ci_icon_for_status(status),
+ class: klass, title: title, data: data
+ end
end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 7a02d0b10d9..33dcee49aee 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -98,28 +98,31 @@ module CommitsHelper
end
def link_to_browse_code(project, commit)
- if current_controller?(:projects, :commits)
- if @repo.blob_at(commit.id, @path)
- return link_to(
- "Browse File",
- namespace_project_blob_path(project.namespace, project,
- tree_join(commit.id, @path)),
- class: "btn btn-default"
- )
- elsif @path.present?
- return link_to(
- "Browse Directory",
- namespace_project_tree_path(project.namespace, project,
- tree_join(commit.id, @path)),
- class: "btn btn-default"
- )
- end
+ if @path.blank?
+ return link_to(
+ "Browse Files",
+ namespace_project_tree_path(project.namespace, project, commit),
+ class: "btn btn-default"
+ )
+ end
+
+ return unless current_controller?(:projects, :commits)
+
+ if @repo.blob_at(commit.id, @path)
+ return link_to(
+ "Browse File",
+ namespace_project_blob_path(project.namespace, project,
+ tree_join(commit.id, @path)),
+ class: "btn btn-default"
+ )
+ elsif @path.present?
+ return link_to(
+ "Browse Directory",
+ namespace_project_tree_path(project.namespace, project,
+ tree_join(commit.id, @path)),
+ class: "btn btn-default"
+ )
end
- link_to(
- "Browse Files",
- namespace_project_tree_path(project.namespace, project, commit),
- class: "btn btn-default"
- )
end
def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 2e82b44437b..8b212b0327a 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -114,9 +114,17 @@ module IssuesHelper
end
def award_user_list(awards, current_user)
- awards.map do |award|
- award.user == current_user ? 'me' : award.user.name
- end.join(', ')
+ names = awards.map do |award|
+ award.user == current_user ? 'You' : award.user.name
+ end
+
+ # Take first 9 OR current user + first 9
+ current_user_name = names.delete('You')
+ names = names.first(9).insert(0, current_user_name).compact
+
+ names << "#{awards.size - names.size} more." if awards.size > names.size
+
+ names.to_sentence
end
def award_active_class(awards, current_user)
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 3ff8be5e284..6c1cc6ef072 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -24,6 +24,7 @@ module NavHelper
current_path?('merge_requests#diffs') ||
current_path?('merge_requests#commits') ||
current_path?('merge_requests#builds') ||
+ current_path?('merge_requests#conflicts') ||
current_path?('issues#show')
if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed"
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index 6f54c42146c..d64e48f774b 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -6,6 +6,11 @@ module Emails
mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
end
+ def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id)
+ setup_issue_mail(issue_id, recipient_id)
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
+ end
+
def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id)
setup_issue_mail(issue_id, recipient_id)
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 9dd11d20ea6..95810b0ac6e 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -6,6 +6,11 @@ module Emails
mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id))
end
+ def new_mention_in_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
+ end
+
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id)
diff --git a/app/models/ability.rb b/app/models/ability.rb
index d9113ffd99a..55265c3cfcb 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -90,6 +90,8 @@ class Ability
if project && project.public?
rules = [
:read_project,
+ :read_board,
+ :read_list,
:read_wiki,
:read_label,
:read_milestone,
@@ -228,6 +230,8 @@ class Ability
:read_project,
:read_wiki,
:read_issue,
+ :read_board,
+ :read_list,
:read_label,
:read_milestone,
:read_project_snippet,
@@ -249,6 +253,7 @@ class Ability
:update_issue,
:admin_issue,
:admin_label,
+ :admin_list,
:read_commit_status,
:read_build,
:read_container_image,
diff --git a/app/models/board.rb b/app/models/board.rb
new file mode 100644
index 00000000000..3240c4bede3
--- /dev/null
+++ b/app/models/board.rb
@@ -0,0 +1,7 @@
+class Board < ActiveRecord::Base
+ belongs_to :project
+
+ has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all
+
+ validates :project, presence: true
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 4c84f4c21c5..ed056a07a49 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -97,7 +97,7 @@ module Ci
end
def playable?
- project.builds_enabled? && commands.present? && manual?
+ project.builds_enabled? && commands.present? && manual? && skipped?
end
def play(current_user = nil)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 130afeb724e..c360a6ff729 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -78,6 +78,10 @@ module Ci
CommitStatus.where(pipeline: pluck(:id)).stages
end
+ def stages_with_latest_statuses
+ statuses.latest.order(:stage_idx).group_by(&:stage)
+ end
+
def project_id
project.id
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index c816deb4e0c..e02a3d54c36 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -75,7 +75,7 @@ class DiffNote < Note
private
def supported?
- !self.for_merge_request? || self.noteable.support_new_diff_notes?
+ !self.for_merge_request? || self.noteable.has_complete_diff_refs?
end
def noteable_diff_refs
diff --git a/app/models/label.rb b/app/models/label.rb
index 35e678001dc..a23140b7d64 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -13,6 +13,8 @@ class Label < ActiveRecord::Base
default_value_for :color, DEFAULT_COLOR
belongs_to :project
+
+ has_many :lists, dependent: :destroy
has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
diff --git a/app/models/list.rb b/app/models/list.rb
new file mode 100644
index 00000000000..eb87decdbc8
--- /dev/null
+++ b/app/models/list.rb
@@ -0,0 +1,34 @@
+class List < ActiveRecord::Base
+ belongs_to :board
+ belongs_to :label
+
+ enum list_type: { backlog: 0, label: 1, done: 2 }
+
+ validates :board, :list_type, presence: true
+ validates :label, :position, presence: true, if: :label?
+ validates :label_id, uniqueness: { scope: :board_id }, if: :label?
+ validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :label?
+
+ before_destroy :can_be_destroyed
+
+ scope :destroyable, -> { where(list_type: list_types[:label]) }
+ scope :movable, -> { where(list_type: list_types[:label]) }
+
+ def destroyable?
+ label?
+ end
+
+ def movable?
+ label?
+ end
+
+ def title
+ label? ? label.name : list_type.humanize
+ end
+
+ private
+
+ def can_be_destroyed
+ destroyable?
+ end
+end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index fe799382fd0..4304ef04767 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -674,10 +674,21 @@ class MergeRequest < ActiveRecord::Base
diverged_commits_count > 0
end
+ def commits_sha
+ commits.map(&:sha)
+ end
+
def pipeline
@pipeline ||= source_project.pipeline(diff_head_sha, source_branch) if diff_head_sha && source_project
end
+ def all_pipelines
+ @all_pipelines ||=
+ if diff_head_sha && source_project
+ source_project.pipelines.order(id: :desc).where(sha: commits_sha, ref: source_branch)
+ end
+ end
+
def merge_commit
@merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
end
@@ -690,12 +701,12 @@ class MergeRequest < ActiveRecord::Base
merge_commit
end
- def support_new_diff_notes?
+ def has_complete_diff_refs?
diff_sha_refs && diff_sha_refs.complete?
end
def update_diff_notes_positions(old_diff_refs:, new_diff_refs:)
- return unless support_new_diff_notes?
+ return unless has_complete_diff_refs?
return if new_diff_refs == old_diff_refs
active_diff_notes = self.notes.diff_notes.select do |note|
@@ -723,4 +734,26 @@ class MergeRequest < ActiveRecord::Base
def keep_around_commit
project.repository.keep_around(self.merge_commit_sha)
end
+
+ def conflicts
+ @conflicts ||= Gitlab::Conflict::FileCollection.new(self)
+ end
+
+ def conflicts_can_be_resolved_by?(user)
+ access = ::Gitlab::UserAccess.new(user, project: source_project)
+ access.can_push_to_branch?(source_branch)
+ end
+
+ def conflicts_can_be_resolved_in_ui?
+ return @conflicts_can_be_resolved_in_ui if defined?(@conflicts_can_be_resolved_in_ui)
+
+ return @conflicts_can_be_resolved_in_ui = false unless cannot_be_merged?
+ return @conflicts_can_be_resolved_in_ui = false unless has_complete_diff_refs?
+
+ begin
+ @conflicts_can_be_resolved_in_ui = conflicts.files.each(&:lines)
+ rescue Gitlab::Conflict::Parser::ParserError, Gitlab::Conflict::FileCollection::ConflictSideMissing
+ @conflicts_can_be_resolved_in_ui = false
+ end
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index eefdae35615..043da030468 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -62,6 +62,8 @@ class Project < ActiveRecord::Base
belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id'
belongs_to :namespace
+ has_one :board, dependent: :destroy
+
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
# Project services
diff --git a/app/models/repository.rb b/app/models/repository.rb
index e56bac509a4..2494c266cd2 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -391,6 +391,8 @@ class Repository
expire_exists_cache
expire_root_ref_cache
expire_emptiness_caches
+
+ repository_event(:create_repository)
end
# Runs code just before a repository is deleted.
@@ -407,6 +409,8 @@ class Repository
expire_root_ref_cache
expire_emptiness_caches
expire_exists_cache
+
+ repository_event(:remove_repository)
end
# Runs code just before the HEAD of a repository is changed.
@@ -414,6 +418,8 @@ class Repository
# Cached divergent commit counts are based on repository head
expire_branch_cache
expire_root_ref_cache
+
+ repository_event(:change_default_branch)
end
# Runs code before pushing (= creating or removing) a tag.
@@ -421,12 +427,16 @@ class Repository
expire_cache
expire_tags_cache
expire_tag_count_cache
+
+ repository_event(:push_tag)
end
# Runs code before removing a tag.
def before_remove_tag
expire_tags_cache
expire_tag_count_cache
+
+ repository_event(:remove_tag)
end
def before_import
@@ -443,6 +453,8 @@ class Repository
# Runs code after a new commit has been pushed.
def after_push_commit(branch_name, revision)
expire_cache(branch_name, revision)
+
+ repository_event(:push_commit, branch: branch_name)
end
# Runs code after a new branch has been created.
@@ -450,11 +462,15 @@ class Repository
expire_branches_cache
expire_has_visible_content_cache
expire_branch_count_cache
+
+ repository_event(:push_branch)
end
# Runs code before removing an existing branch.
def before_remove_branch
expire_branches_cache
+
+ repository_event(:remove_branch)
end
# Runs code after an existing branch has been removed.
@@ -869,6 +885,14 @@ class Repository
end
end
+ def resolve_conflicts(user, branch, params)
+ commit_with_hooks(user, branch) do
+ committer = user_to_committer(user)
+
+ Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer))
+ end
+ end
+
def check_revert_content(commit, base_branch)
source_sha = find_branch(base_branch).target.sha
args = [commit.id, source_sha]
@@ -1059,4 +1083,8 @@ class Repository
def keep_around_ref_name(sha)
"refs/keep-around/#{sha}"
end
+
+ def repository_event(event, tags = {})
+ Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags))
+ end
end
diff --git a/app/models/u2f_registration.rb b/app/models/u2f_registration.rb
index 00b19686d48..808acec098f 100644
--- a/app/models/u2f_registration.rb
+++ b/app/models/u2f_registration.rb
@@ -3,18 +3,19 @@
class U2fRegistration < ActiveRecord::Base
belongs_to :user
- def self.register(user, app_id, json_response, challenges)
+ def self.register(user, app_id, params, challenges)
u2f = U2F::U2F.new(app_id)
registration = self.new
begin
- response = U2F::RegisterResponse.load_from_json(json_response)
+ response = U2F::RegisterResponse.load_from_json(params[:device_response])
registration_data = u2f.register!(challenges, response)
registration.update(certificate: registration_data.certificate,
key_handle: registration_data.key_handle,
public_key: registration_data.public_key,
counter: registration_data.counter,
- user: user)
+ user: user,
+ name: params[:name])
rescue JSON::ParserError, NoMethodError, ArgumentError
registration.errors.add(:base, 'Your U2F device did not send a valid JSON response.')
rescue U2F::Error => e
diff --git a/app/services/boards/base_service.rb b/app/services/boards/base_service.rb
new file mode 100644
index 00000000000..b2069ca825a
--- /dev/null
+++ b/app/services/boards/base_service.rb
@@ -0,0 +1,5 @@
+module Boards
+ class BaseService < ::BaseService
+ delegate :board, to: :project
+ end
+end
diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb
new file mode 100644
index 00000000000..072a0749285
--- /dev/null
+++ b/app/services/boards/create_service.rb
@@ -0,0 +1,16 @@
+module Boards
+ class CreateService < Boards::BaseService
+ def execute
+ create_board! unless project.board.present?
+ project.board
+ end
+
+ private
+
+ def create_board!
+ project.create_board
+ project.board.lists.create(list_type: :backlog)
+ project.board.lists.create(list_type: :done)
+ end
+ end
+end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
new file mode 100644
index 00000000000..435a8c6e681
--- /dev/null
+++ b/app/services/boards/issues/list_service.rb
@@ -0,0 +1,63 @@
+module Boards
+ module Issues
+ class ListService < Boards::BaseService
+ def execute
+ issues = IssuesFinder.new(current_user, filter_params).execute
+ issues = without_board_labels(issues) unless list.movable?
+ issues = with_list_label(issues) if list.movable?
+ issues
+ end
+
+ private
+
+ def list
+ @list ||= board.lists.find(params[:id])
+ end
+
+ def filter_params
+ set_default_scope
+ set_default_sort
+ set_project
+ set_state
+
+ params
+ end
+
+ def set_default_scope
+ params[:scope] = 'all'
+ end
+
+ def set_default_sort
+ params[:sort] = 'priority'
+ end
+
+ def set_project
+ params[:project_id] = project.id
+ end
+
+ def set_state
+ params[:state] = list.done? ? 'closed' : 'opened'
+ end
+
+ def board_label_ids
+ @board_label_ids ||= board.lists.movable.pluck(:label_id)
+ end
+
+ def without_board_labels(issues)
+ return issues unless board_label_ids.any?
+
+ issues.where.not(
+ LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id")
+ .where(label_id: board_label_ids).limit(1).arel.exists
+ )
+ end
+
+ def with_list_label(issues)
+ issues.where(
+ LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id")
+ .where("label_links.label_id = ?", list.label_id).limit(1).arel.exists
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
new file mode 100644
index 00000000000..84dc3f70e76
--- /dev/null
+++ b/app/services/boards/issues/move_service.rb
@@ -0,0 +1,59 @@
+module Boards
+ module Issues
+ class MoveService < Boards::BaseService
+ def execute(issue)
+ return false unless can?(current_user, :update_issue, issue)
+ return false unless valid_move?
+
+ update_service.execute(issue)
+ end
+
+ private
+
+ def valid_move?
+ moving_from_list.present? && moving_to_list.present? &&
+ moving_from_list != moving_to_list
+ end
+
+ def moving_from_list
+ @moving_from_list ||= board.lists.find_by(id: params[:from_list_id])
+ end
+
+ def moving_to_list
+ @moving_to_list ||= board.lists.find_by(id: params[:to_list_id])
+ end
+
+ def update_service
+ ::Issues::UpdateService.new(project, current_user, issue_params)
+ end
+
+ def issue_params
+ {
+ add_label_ids: add_label_ids,
+ remove_label_ids: remove_label_ids,
+ state_event: issue_state
+ }
+ end
+
+ def issue_state
+ return 'reopen' if moving_from_list.done?
+ return 'close' if moving_to_list.done?
+ end
+
+ def add_label_ids
+ [moving_to_list.label_id].compact
+ end
+
+ def remove_label_ids
+ label_ids =
+ if moving_to_list.movable?
+ moving_from_list.label_id
+ else
+ board.lists.movable.pluck(:label_id)
+ end
+
+ Array(label_ids).compact
+ end
+ end
+ end
+end
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
new file mode 100644
index 00000000000..5cb408b9d20
--- /dev/null
+++ b/app/services/boards/lists/create_service.rb
@@ -0,0 +1,22 @@
+module Boards
+ module Lists
+ class CreateService < Boards::BaseService
+ def execute
+ List.transaction do
+ create_list_at(next_position)
+ end
+ end
+
+ private
+
+ def next_position
+ max_position = board.lists.movable.maximum(:position)
+ max_position.nil? ? 0 : max_position.succ
+ end
+
+ def create_list_at(position)
+ board.lists.create(params.merge(list_type: :label, position: position))
+ end
+ end
+ end
+end
diff --git a/app/services/boards/lists/destroy_service.rb b/app/services/boards/lists/destroy_service.rb
new file mode 100644
index 00000000000..25da3bfb56d
--- /dev/null
+++ b/app/services/boards/lists/destroy_service.rb
@@ -0,0 +1,25 @@
+module Boards
+ module Lists
+ class DestroyService < Boards::BaseService
+ def execute(list)
+ return false unless list.destroyable?
+
+ list.with_lock do
+ decrement_higher_lists(list)
+ remove_list(list)
+ end
+ end
+
+ private
+
+ def decrement_higher_lists(list)
+ board.lists.movable.where('position > ?', list.position)
+ .update_all('position = position - 1')
+ end
+
+ def remove_list(list)
+ list.destroy
+ end
+ end
+ end
+end
diff --git a/app/services/boards/lists/generate_service.rb b/app/services/boards/lists/generate_service.rb
new file mode 100644
index 00000000000..1c48b9786e4
--- /dev/null
+++ b/app/services/boards/lists/generate_service.rb
@@ -0,0 +1,36 @@
+module Boards
+ module Lists
+ class GenerateService < Boards::BaseService
+ def execute
+ return false unless board.lists.movable.empty?
+
+ List.transaction do
+ label_params.each { |params| create_list(params) }
+ end
+
+ true
+ end
+
+ private
+
+ def create_list(params)
+ label = find_or_create_label(params)
+ Lists::CreateService.new(project, current_user, label_id: label.id).execute
+ end
+
+ def find_or_create_label(params)
+ project.labels.create_with(color: params[:color])
+ .find_or_create_by(name: params[:name])
+ end
+
+ def label_params
+ [
+ { name: 'Development', color: '#5CB85C' },
+ { name: 'Testing', color: '#F0AD4E' },
+ { name: 'Production', color: '#FF5F00' },
+ { name: 'Ready', color: '#FF0000' }
+ ]
+ end
+ end
+ end
+end
diff --git a/app/services/boards/lists/move_service.rb b/app/services/boards/lists/move_service.rb
new file mode 100644
index 00000000000..020ff69f4a7
--- /dev/null
+++ b/app/services/boards/lists/move_service.rb
@@ -0,0 +1,51 @@
+module Boards
+ module Lists
+ class MoveService < Boards::BaseService
+ def execute(list)
+ @old_position = list.position
+ @new_position = params[:position]
+
+ return false unless list.movable?
+ return false unless valid_move?
+
+ list.with_lock do
+ reorder_intermediate_lists
+ update_list_position(list)
+ end
+ end
+
+ private
+
+ attr_reader :old_position, :new_position
+
+ def valid_move?
+ new_position.present? && new_position != old_position &&
+ new_position >= 0 && new_position < board.lists.movable.size
+ end
+
+ def reorder_intermediate_lists
+ if old_position < new_position
+ decrement_intermediate_lists
+ else
+ increment_intermediate_lists
+ end
+ end
+
+ def decrement_intermediate_lists
+ board.lists.movable.where('position > ?', old_position)
+ .where('position <= ?', new_position)
+ .update_all('position = position - 1')
+ end
+
+ def increment_intermediate_lists
+ board.lists.movable.where('position >= ?', new_position)
+ .where('position < ?', old_position)
+ .update_all('position = position + 1')
+ end
+
+ def update_list_position(list)
+ list.update_attribute(:position, new_position)
+ end
+ end
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 2d96efe1042..b0ea7c905f8 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -104,11 +104,12 @@ class IssuableBaseService < BaseService
change_subscription(issuable)
filter_params
old_labels = issuable.labels.to_a
+ old_mentioned_users = issuable.mentioned_users.to_a
if params.present? && update_issuable(issuable, params)
issuable.reset_events_cache
handle_common_system_notes(issuable, old_labels: old_labels)
- handle_changes(issuable, old_labels: old_labels)
+ handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index c7d406cc331..a2111b3806b 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -4,7 +4,7 @@ module Issues
update(issue)
end
- def handle_changes(issue, old_labels: [])
+ def handle_changes(issue, old_labels: [], old_mentioned_users: [])
if has_changes?(issue, old_labels: old_labels)
todo_service.mark_pending_todos_as_done(issue, current_user)
end
@@ -32,6 +32,11 @@ module Issues
if added_labels.present?
notification_service.relabeled_issue(issue, added_labels, current_user)
end
+
+ added_mentions = issue.mentioned_users - old_mentioned_users
+ if added_mentions.present?
+ notification_service.new_mentions_in_issue(issue, added_mentions, current_user)
+ end
end
def reopen_service
diff --git a/app/services/merge_requests/resolve_service.rb b/app/services/merge_requests/resolve_service.rb
new file mode 100644
index 00000000000..adc71b0c2bc
--- /dev/null
+++ b/app/services/merge_requests/resolve_service.rb
@@ -0,0 +1,31 @@
+module MergeRequests
+ class ResolveService < MergeRequests::BaseService
+ attr_accessor :conflicts, :rugged, :merge_index
+
+ def execute(merge_request)
+ @conflicts = merge_request.conflicts
+ @rugged = project.repository.rugged
+ @merge_index = conflicts.merge_index
+
+ conflicts.files.each do |file|
+ write_resolved_file_to_index(file, params[:sections])
+ end
+
+ commit_params = {
+ message: params[:commit_message] || conflicts.default_commit_message,
+ parents: [conflicts.our_commit, conflicts.their_commit].map(&:oid),
+ tree: merge_index.write_tree(rugged)
+ }
+
+ project.repository.resolve_conflicts(current_user, merge_request.source_branch, commit_params)
+ end
+
+ def write_resolved_file_to_index(file, resolutions)
+ new_file = file.resolve_lines(resolutions).map(&:text).join("\n")
+ our_path = file.our_path
+
+ merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode)
+ merge_index.conflict_remove(our_path)
+ end
+ end
+end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 026a37997d4..30c5f24988c 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -16,7 +16,7 @@ module MergeRequests
update(merge_request)
end
- def handle_changes(merge_request, old_labels: [])
+ def handle_changes(merge_request, old_labels: [], old_mentioned_users: [])
if has_changes?(merge_request, old_labels: old_labels)
todo_service.mark_pending_todos_as_done(merge_request, current_user)
end
@@ -55,6 +55,15 @@ module MergeRequests
current_user
)
end
+
+ added_mentions = merge_request.mentioned_users - old_mentioned_users
+ if added_mentions.present?
+ notification_service.new_mentions_in_merge_request(
+ merge_request,
+ added_mentions,
+ current_user
+ )
+ end
end
def reopen_service
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index ab6e51209ee..2291bc0f127 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -35,6 +35,20 @@ class NotificationService
new_resource_email(issue, issue.project, :new_issue_email)
end
+ # When issue text is updated, we should send an email to:
+ #
+ # * newly mentioned project team members with notification level higher than Participating
+ #
+ def new_mentions_in_issue(issue, new_mentioned_users, current_user)
+ new_mentions_in_resource_email(
+ issue,
+ issue.project,
+ new_mentioned_users,
+ current_user,
+ :new_mention_in_issue_email
+ )
+ end
+
# When we close an issue we should send an email to:
#
# * issue author if their notification level is not Disabled
@@ -75,6 +89,20 @@ class NotificationService
new_resource_email(merge_request, merge_request.target_project, :new_merge_request_email)
end
+ # When merge request text is updated, we should send an email to:
+ #
+ # * newly mentioned project team members with notification level higher than Participating
+ #
+ def new_mentions_in_merge_request(merge_request, new_mentioned_users, current_user)
+ new_mentions_in_resource_email(
+ merge_request,
+ merge_request.target_project,
+ new_mentioned_users,
+ current_user,
+ :new_mention_in_merge_request_email
+ )
+ end
+
# When we reassign a merge_request we should send an email to:
#
# * merge_request old assignee if their notification level is not Disabled
@@ -177,7 +205,7 @@ class NotificationService
# build notify method like 'note_commit_email'
notify_method = "note_#{note.noteable_type.underscore}_email".to_sym
-
+
recipients.each do |recipient|
mailer.send(notify_method, recipient.id, note.id).deliver_later
end
@@ -471,6 +499,15 @@ class NotificationService
end
end
+ def new_mentions_in_resource_email(target, project, new_mentioned_users, current_user, method)
+ recipients = build_recipients(target, project, current_user, action: "new")
+ recipients = recipients & new_mentioned_users
+
+ recipients.each do |recipient|
+ mailer.send(method, recipient.id, target.id, current_user.id).deliver_later
+ end
+ end
+
def close_resource_email(target, project, current_user, method)
action = method == :merged_merge_request_email ? "merge" : "close"
recipients = build_recipients(target, project, current_user, action: action)
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index eb833dd82ac..daf4339cb48 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -142,7 +142,11 @@ class TodoService
# When user marks some todos as done
def mark_todos_as_done(todos, current_user)
- todos = current_user.todos.where(id: todos.map(&:id)) unless todos.respond_to?(:update_all)
+ mark_todos_as_done_by_ids(todos.select(&:id), current_user)
+ end
+
+ def mark_todos_as_done_by_ids(ids, current_user)
+ todos = current_user.todos.where(id: ids)
marked_todos = todos.update_all(state: :done)
current_user.update_todos_count_cache
diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml
index dd2e7ebd030..56bf6194914 100644
--- a/app/views/admin/abuse_reports/_abuse_report.html.haml
+++ b/app/views/admin/abuse_reports/_abuse_report.html.haml
@@ -1,6 +1,8 @@
- reporter = abuse_report.reporter
- user = abuse_report.user
%tr
+ %th.visible-xs-block.visible-sm-block
+ %strong User
%td
- if user
= link_to user.name, user
@@ -9,6 +11,7 @@
- else
(removed)
%td
+ %strong.subheading.visible-xs-block.visible-sm-block Reported by
- if reporter
= link_to reporter.name, reporter
- else
@@ -16,16 +19,16 @@
.light.small
= time_ago_with_tooltip(abuse_report.created_at)
%td
- = markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter)
+ %strong.subheading.visible-xs-block.visible-sm-block Message
+ .message
+ = markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter)
%td
- if user
= link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true),
- data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, remote: true, method: :delete, class: "btn btn-xs btn-remove js-remove-tr"
-
- %td
+ data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, remote: true, method: :delete, class: "btn btn-sm btn-block btn-remove js-remove-tr"
- if user && !user.blocked?
- = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs"
+ = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-sm btn-block"
- else
- .btn.btn-xs.disabled
+ .btn.btn-sm.disabled.btn-block
Already Blocked
- = link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr"
+ = link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-sm btn-block btn-close js-remove-tr"
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index bc4a9cedb2c..7bbc75db9ff 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -1,17 +1,20 @@
-- page_title "Abuse Reports"
+- page_title 'Abuse Reports'
%h3.page-title Abuse Reports
%hr
-- if @abuse_reports.present?
- .table-holder
- %table.table
- %thead
- %tr
- %th User
- %th Reported by
- %th Message
- %th Primary action
- %th
- = render @abuse_reports
- = paginate @abuse_reports
-- else
- %h4 There are no abuse reports
+.abuse-reports
+ - if @abuse_reports.present?
+ .table-holder
+ %table.table
+ %thead.hidden-sm.hidden-xs
+ %tr
+ %th User
+ %th Reported by
+ %th.wide Message
+ %th Action
+ = render @abuse_reports
+ - else
+ .no-reports
+ %span.pull-left
+ There are no abuse reports!
+ .pull-left
+ = emoji_icon 'tada'
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index a1a71c2fb33..bf50633af24 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -23,7 +23,6 @@
= render "layouts/broadcast"
= render "layouts/flash"
= yield :flash_message
- %div{ class: (container_class unless @no_container) }
+ %div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
.content
- .clearfix
- = yield
+ = yield
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 33cedaaf2ee..15a94ac23c5 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,5 +1,5 @@
!!! 5
-%html{ lang: "en"}
+%html{ lang: "en", class: "#{page_class}" }
= render "layouts/head"
%body{class: "#{user_application_theme}", data: {page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}"}}
= Gon::Base.render_data
diff --git a/app/views/notify/new_mention_in_issue_email.html.haml b/app/views/notify/new_mention_in_issue_email.html.haml
new file mode 100644
index 00000000000..4f3d36bd9ca
--- /dev/null
+++ b/app/views/notify/new_mention_in_issue_email.html.haml
@@ -0,0 +1,12 @@
+%p
+ You have been mentioned in an issue.
+
+- if current_application_settings.email_author_in_body
+ %div
+ #{link_to @issue.author_name, user_url(@issue.author)} wrote:
+-if @issue.description
+ = markdown(@issue.description, pipeline: :email, author: @issue.author)
+
+- if @issue.assignee_id.present?
+ %p
+ Assignee: #{@issue.assignee_name}
diff --git a/app/views/notify/new_mention_in_issue_email.text.erb b/app/views/notify/new_mention_in_issue_email.text.erb
new file mode 100644
index 00000000000..457e94b4800
--- /dev/null
+++ b/app/views/notify/new_mention_in_issue_email.text.erb
@@ -0,0 +1,7 @@
+You have been mentioned in an issue.
+
+Issue <%= @issue.iid %>: <%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)) %>
+Author: <%= @issue.author_name %>
+Assignee: <%= @issue.assignee_name %>
+
+<%= @issue.description %>
diff --git a/app/views/notify/new_mention_in_merge_request_email.html.haml b/app/views/notify/new_mention_in_merge_request_email.html.haml
new file mode 100644
index 00000000000..32aedb9e6b9
--- /dev/null
+++ b/app/views/notify/new_mention_in_merge_request_email.html.haml
@@ -0,0 +1,15 @@
+%p
+ You have been mentioned in Merge Request #{@merge_request.to_reference}
+
+- if current_application_settings.email_author_in_body
+ %div
+ #{link_to @merge_request.author_name, user_url(@merge_request.author)} wrote:
+%p.details
+ != merge_path_description(@merge_request, '&rarr;')
+
+- if @merge_request.assignee_id.present?
+ %p
+ Assignee: #{@merge_request.author_name} &rarr; #{@merge_request.assignee_name}
+
+-if @merge_request.description
+ = markdown(@merge_request.description, pipeline: :email, author: @merge_request.author)
diff --git a/app/views/notify/new_mention_in_merge_request_email.text.erb b/app/views/notify/new_mention_in_merge_request_email.text.erb
new file mode 100644
index 00000000000..5bf0282e097
--- /dev/null
+++ b/app/views/notify/new_mention_in_merge_request_email.text.erb
@@ -0,0 +1,9 @@
+You have been mentioned in Merge Request <%= @merge_request.to_reference %>
+
+<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)) %>
+
+<%= merge_path_description(@merge_request, 'to') %>
+Author: <%= @merge_request.author_name %>
+Assignee: <%= @merge_request.assignee_name %>
+
+<%= @merge_request.description %>
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index c161ecc3463..4dc39a72225 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -76,7 +76,7 @@
- if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob)
%table.code.white
- diff_file.highlighted_diff_lines.each do |line|
- = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true
+ = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true, email: true
- else
No preview for this file type
%br
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 366f1fed35b..03ac739ade5 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -60,13 +60,38 @@
two-factor authentication app before a U2F device. That way you'll always be able to
log in - even when you're using an unsupported browser.
.col-lg-9
- %p
- - if @registration_key_handles.present?
- = icon "check inverse", base: "circle", class: "text-success", text: "You have #{pluralize(@registration_key_handles.size, 'U2F device')} registered with GitLab."
- if @u2f_registration.errors.present?
= form_errors(@u2f_registration)
= render "u2f/register"
+ %hr
+
+ %h5 U2F Devices (#{@u2f_registrations.length})
+
+ - if @u2f_registrations.present?
+ .table-responsive
+ %table.table.table-bordered.u2f-registrations
+ %colgroup
+ %col{ width: "50%" }
+ %col{ width: "30%" }
+ %col{ width: "20%" }
+ %thead
+ %tr
+ %th Name
+ %th Registered On
+ %th
+ %tbody
+ - @u2f_registrations.each do |registration|
+ %tr
+ %td= registration.name.presence || "<no name set>"
+ %td= registration.created_at.to_date.to_s(:medium)
+ %td= link_to "Delete", profile_u2f_registration_path(registration), method: :delete, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to delete this device? This action cannot be undone." }
+
+ - else
+ .settings-message.text-center
+ You don't have any U2F devices registered yet.
+
+
- if two_factor_skippable?
:javascript
var button = "<a class='btn btn-xs btn-warning pull-right' data-method='patch' href='#{skip_profile_two_factor_auth_path}'>Configure it later</a>";
diff --git a/app/views/projects/boards/components/_blank_state.html.haml b/app/views/projects/boards/components/_blank_state.html.haml
new file mode 100644
index 00000000000..97eb952eff1
--- /dev/null
+++ b/app/views/projects/boards/components/_blank_state.html.haml
@@ -0,0 +1,15 @@
+%board-blank-state{ "inline-template" => true,
+ "v-if" => "list.id == 'blank'" }
+ .board-blank-state
+ %p
+ Add the following default lists to your Issue Board with one click:
+ %ul.board-blank-state-list
+ %li{ "v-for" => "label in predefinedLabels" }
+ %span.label-color{ ":style" => "{ backgroundColor: label.color } " }
+ {{ label.title }}
+ %p
+ Starting out with the default set of lists will get you right on the way to making the most of your board.
+ %button.btn.btn-create.btn-inverted.btn-block{ type: "button", "@click.stop" => "addDefaultLists" }
+ Add default lists
+ %button.btn.btn-default.btn-block{ type: "button", "@click.stop" => "clearBlankState" }
+ Nevermind, I'll use my own
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
new file mode 100644
index 00000000000..f8ebf397ee2
--- /dev/null
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -0,0 +1,44 @@
+%board{ "inline-template" => true,
+ "v-cloak" => true,
+ "v-for" => "list in state.lists | orderBy 'position'",
+ "v-ref:board" => true,
+ ":list" => "list",
+ ":disabled" => "disabled",
+ ":issue-link-base" => "issueLinkBase",
+ "track-by" => "_uid" }
+ .board{ ":class" => "{ 'is-draggable': !list.preset }",
+ ":data-id" => "list.id" }
+ .board-inner
+ %header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
+ %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
+ = icon("align-justify", class: "board-mobile-handle js-board-drag-handle", "v-if" => "(!disabled && !list.preset)")
+ {{ list.title }}
+ %span.pull-right{ "v-if" => "list.type !== 'blank'" }
+ {{ list.issues.length }}
+ - if can?(current_user, :admin_list, @project)
+ %board-delete{ "inline-template" => true,
+ ":list" => "list",
+ "v-if" => "!list.preset && list.id" }
+ %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
+ = icon("trash")
+ = icon("spinner spin", class: "board-header-loading-spinner pull-right", "v-show" => "list.loadingMore")
+ .board-inner-container.board-search-container{ "v-if" => "list.canSearch()" }
+ %input.form-control{ type: "text", placeholder: "Search issues", "v-model" => "query", "debounce" => "250" }
+ = icon("search", class: "board-search-icon", "v-show" => "!query")
+ %button.board-search-clear-btn{ type: "button", role: "button", "aria-label" => "Clear search", "@click" => "query = ''", "v-show" => "query" }
+ = icon("times", class: "board-search-clear")
+ %board-list{ "inline-template" => true,
+ "v-if" => "list.type !== 'blank'",
+ ":list" => "list",
+ ":issues" => "list.issues",
+ ":loading" => "list.loading",
+ ":disabled" => "disabled",
+ ":issue-link-base" => "issueLinkBase" }
+ .board-list-loading.text-center{ "v-if" => "loading" }
+ = icon("spinner spin")
+ %ul.board-list{ "v-el:list" => true,
+ "v-show" => "!loading",
+ ":data-board" => "list.id" }
+ = render "projects/boards/components/card"
+ - if can?(current_user, :admin_list, @project)
+ = render "projects/boards/components/blank_state"
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
new file mode 100644
index 00000000000..b20c23f6b8e
--- /dev/null
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -0,0 +1,34 @@
+%board-card{ "inline-template" => true,
+ "v-for" => "issue in issues | orderBy 'priority'",
+ "v-ref:issue" => true,
+ ":index" => "$index",
+ ":list" => "list",
+ ":issue" => "issue",
+ ":issue-link-base" => "issueLinkBase",
+ ":disabled" => "disabled",
+ "track-by" => "id" }
+ %li.card{ ":class" => "{ 'user-can-drag': !disabled }",
+ ":index" => "index" }
+ = icon("align-justify", class: "board-mobile-handle js-card-drag-handle", "v-if" => "!disabled")
+ %h4.card-title
+ = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
+ %a{ ":href" => "issueLinkBase + '/' + issue.id",
+ ":title" => "issue.title" }
+ {{ issue.title }}
+ .card-footer
+ %span.card-number
+ = precede '#' do
+ {{ issue.id }}
+ %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
+ type: "button",
+ "v-if" => "(!list.label || label.id !== list.label.id)",
+ "@click" => "filterByLabel(label, $event)",
+ ":style" => "{ backgroundColor: label.color, color: label.textColor }",
+ ":title" => "label.description",
+ data: { container: 'body' } }
+ {{ label.title }}
+ %a.has-tooltip{ ":href" => "'/u/' + issue.assignee.username",
+ ":title" => "'Assigned to ' + issue.assignee.name",
+ "v-if" => "issue.assignee",
+ data: { container: 'body' } }
+ %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
diff --git a/app/views/projects/boards/show.html.haml b/app/views/projects/boards/show.html.haml
new file mode 100644
index 00000000000..edbbd3f3d2a
--- /dev/null
+++ b/app/views/projects/boards/show.html.haml
@@ -0,0 +1,19 @@
+- @no_container = true
+- @content_class = "issue-boards-content"
+- page_title "Boards"
+
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('boards/boards_bundle.js')
+ = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
+
+= render "projects/issues/head"
+
+= render 'shared/issuable/filter', type: :boards
+
+.boards-list#board-app{ "v-cloak" => true,
+ "data-endpoint" => "#{namespace_project_board_path(@project.namespace, @project)}",
+ "data-disabled" => "#{!can?(current_user, :admin_list, @project)}",
+ "data-issue-link-base" => "#{namespace_project_issues_path(@project.namespace, @project)}" }
+ .boards-app-loading.text-center{ "v-if" => "loading" }
+ = icon("spinner spin")
+ = render "projects/boards/components/board"
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index a8bc53c2849..5b0b58e087b 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -11,98 +11,133 @@
%p.build-detail-row
#{@build.coverage}%
- - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
- .block{ class: ("block-first" if !@build.coverage) }
- .title
- Build artifacts
- - if @build.artifacts_expired?
- %p.build-detail-row
- The artifacts were removed
- #{time_ago_with_tooltip(@build.artifacts_expire_at)}
- - elsif @build.artifacts_expire_at
- %p.build-detail-row
- The artifacts will be removed in
- %span.js-artifacts-remove= @build.artifacts_expire_at
+ - builds = @build.pipeline.builds.latest.to_a
+ - statuses = ["failed", "pending", "running", "canceled", "success", "skipped"]
+ - if builds.size > 1
+ .dropdown.build-dropdown
+ .build-light-text Stage
+ %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+ %span.stage-selection More
+ = icon('caret-down')
+ %ul.dropdown-menu
+ - builds.map(&:stage).uniq.each do |stage|
+ %li
+ %a.stage-item= stage
- - if @build.artifacts?
- .btn-group.btn-group-justified{ role: :group }
- - if @build.artifacts_expire_at
- = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
- Keep
+ .builds-container
+ - statuses.each do |build_status|
+ - builds.select{|build| build.status == build_status}.each do |build|
+ .build-job{class: ('active' if build == @build), data: {stage: build.stage}}
+ = link_to namespace_project_build_path(@project.namespace, @project, build) do
+ = icon('check')
+ = ci_icon_for_status(build.status)
+ %span
+ - if build.name
+ = build.name
+ - else
+ = build.id
- = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
- Download
+ - if @build.retried?
+ %li.active
+ %a
+ Build ##{@build.id}
+ &middot;
+ %i.fa.fa-warning
+ This build was retried.
- - if @build.artifacts_metadata?
- = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
- Browse
+ .blocks-container
+ - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
+ .block{ class: ("block-first" if !@build.coverage) }
+ .title
+ Build artifacts
+ - if @build.artifacts_expired?
+ %p.build-detail-row
+ The artifacts were removed
+ #{time_ago_with_tooltip(@build.artifacts_expire_at)}
+ - elsif @build.artifacts_expire_at
+ %p.build-detail-row
+ The artifacts will be removed in
+ %span.js-artifacts-remove= @build.artifacts_expire_at
- .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) }
- .title
- Build details
- - if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right', method: :post
- - if @build.merge_request
- %p.build-detail-row
- %span.build-light-text Merge Request:
- = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request)
- - if @build.duration
- %p.build-detail-row
- %span.build-light-text Duration:
- = time_interval_in_words(@build.duration)
- - if @build.finished_at
- %p.build-detail-row
- %span.build-light-text Finished:
- #{time_ago_with_tooltip(@build.finished_at)}
- - if @build.erased_at
- %p.build-detail-row
- %span.build-light-text Erased:
- #{time_ago_with_tooltip(@build.erased_at)}
- %p.build-detail-row
- %span.build-light-text Runner:
- - if @build.runner && current_user && current_user.admin
- = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id)
- - elsif @build.runner
- \##{@build.runner.id}
- .btn-group.btn-group-justified{ role: :group }
- - if @build.has_trace?
- = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default'
- - if @build.active?
- = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
- - if can?(current_user, :update_build, @project) && @build.erasable?
- = link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
- class: "btn btn-sm btn-default", method: :post,
- data: { confirm: "Are you sure you want to erase this build?" } do
- Erase
+ - if @build.artifacts?
+ .btn-group.btn-group-justified{ role: :group }
+ - if @build.artifacts_expire_at
+ = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
+ Keep
+
+ = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
+ Download
- - if @build.trigger_request
- .build-widget
- %h4.title
- Trigger
+ - if @build.artifacts_metadata?
+ = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
+ Browse
- %p
- %span.build-light-text Token:
- #{@build.trigger_request.trigger.short_token}
+ .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) }
+ .title
+ Build details
+ - if can?(current_user, :update_build, @build) && @build.retryable?
+ = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right', method: :post
+ - if @build.merge_request
+ %p.build-detail-row
+ %span.build-light-text Merge Request:
+ = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request)
+ - if @build.duration
+ %p.build-detail-row
+ %span.build-light-text Duration:
+ = time_interval_in_words(@build.duration)
+ - if @build.finished_at
+ %p.build-detail-row
+ %span.build-light-text Finished:
+ #{time_ago_with_tooltip(@build.finished_at)}
+ - if @build.erased_at
+ %p.build-detail-row
+ %span.build-light-text Erased:
+ #{time_ago_with_tooltip(@build.erased_at)}
+ %p.build-detail-row
+ %span.build-light-text Runner:
+ - if @build.runner && current_user && current_user.admin
+ = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id)
+ - elsif @build.runner
+ \##{@build.runner.id}
+ .btn-group.btn-group-justified{ role: :group }
+ - if @build.has_trace?
+ = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default'
+ - if @build.active?
+ = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
+ - if can?(current_user, :update_build, @project) && @build.erasable?
+ = link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
+ class: "btn btn-sm btn-default", method: :post,
+ data: { confirm: "Are you sure you want to erase this build?" } do
+ Erase
+
+ - if @build.trigger_request
+ .build-widget
+ %h4.title
+ Trigger
- - if @build.trigger_request.variables
%p
- %span.build-light-text Variables:
+ %span.build-light-text Token:
+ #{@build.trigger_request.trigger.short_token}
+ - if @build.trigger_request.variables
+ %p
+ %span.build-light-text Variables:
- - @build.trigger_request.variables.each do |key, value|
- %code
- #{key}=#{value}
- .block
- .title
- Commit title
- %p.build-light-text.append-bottom-0
- #{@build.pipeline.git_commit_title}
+ - @build.trigger_request.variables.each do |key, value|
+ %code
+ #{key}=#{value}
- - if @build.tags.any?
.block
.title
- Tags
- - @build.tag_list.each do |tag|
- %span.label.label-primary
- = tag
+ Commit title
+ %p.build-light-text.append-bottom-0
+ #{@build.pipeline.git_commit_title}
+
+ - if @build.tags.any?
+ .block
+ .title
+ Tags
+ - @build.tag_list.each do |tag|
+ %span.label.label-primary
+ = tag
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 4421f3b9562..e4d41288aa6 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -5,26 +5,6 @@
.build-page
= render "header"
- - builds = @build.pipeline.builds.latest.to_a
- - if builds.size > 1
- %ul.nav-links.no-top.no-bottom
- - builds.each do |build|
- %li{class: ('active' if build == @build) }
- = link_to namespace_project_build_path(@project.namespace, @project, build) do
- = ci_icon_for_status(build.status)
- %span
- - if build.name
- = build.name
- - else
- = build.id
-
- - if @build.retried?
- %li.active
- %a
- Build ##{@build.id}
- &middot;
- %i.fa.fa-warning
- This build was retried.
- if @build.stuck?
- unless @build.any_runners_online?
.bs-callout.bs-callout-warning
@@ -67,4 +47,10 @@
= render "sidebar"
:javascript
- new Build("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}", "#{@build.status}", "#{trace_with_state[:state]}")
+ new Build({
+ page_url: "#{namespace_project_build_url(@project.namespace, @project, @build)}",
+ build_url: "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}",
+ build_status: "#{@build.status}",
+ build_stage: "#{@build.stage}",
+ state1: "#{trace_with_state[:state]}"
+ })
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index 71cf5582a4c..311583037e5 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -1,10 +1,10 @@
- if current_user
= link_to toggle_star_namespace_project_path(@project.namespace, @project), { class: 'btn star-btn toggle-star has-tooltip', method: :post, remote: true, title: current_user.starred?(@project) ? 'Unstar project' : 'Star project' } do
- if current_user.starred?(@project)
- = icon('star fw')
+ = icon('star')
%span.starred Unstar
- else
- = icon('star-o fw')
+ = icon('star-o')
%span Star
%div.count-with-arrow
%span.arrow
@@ -13,7 +13,7 @@
- else
= link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: 'You must sign in to star a project' do
- = icon('star fw')
+ = icon('star')
Star
%div.count-with-arrow
%span.arrow
diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
new file mode 100644
index 00000000000..04cbd0c3591
--- /dev/null
+++ b/app/views/projects/ci/builds/_build_pipeline.html.haml
@@ -0,0 +1,14 @@
+- is_playable = subject.playable? && can?(current_user, :update_build, @project)
+%li.build{class: ("playable" if is_playable)}
+ .build-content
+ - if is_playable
+ = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do
+ = render_status_with_link('build', 'play')
+ = subject.name
+ - elsif can?(current_user, :read_build, @project)
+ = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do
+ = render_status_with_link('build', subject.status)
+ = subject.name
+ - else
+ = render_status_with_link('build', subject.status)
+ = ci_icon_for_status(subject.status)
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index 78709a92aed..be387201f8d 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -2,19 +2,21 @@
%tr.commit
%td.commit-link
= link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do
- = ci_status_with_icon(status)
-
-
+ - if defined?(status_icon_only) && status_icon_only
+ = ci_icon_for_status(status)
+ - else
+ = ci_status_with_icon(status)
%td
.branch-commit
= link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do
%span ##{pipeline.id}
- if pipeline.ref
- .icon-container
- = pipeline.tag? ? icon('tag') : icon('code-fork')
- = link_to pipeline.ref, namespace_project_commits_path(@project.namespace, @project, pipeline.ref), class: "monospace branch-name"
- .icon-container
- = custom_icon("icon_commit")
+ - unless defined?(hide_branch) && hide_branch
+ .icon-container
+ = pipeline.tag? ? icon('tag') : icon('code-fork')
+ = link_to pipeline.ref, namespace_project_commits_path(@project.namespace, @project, pipeline.ref), class: "monospace branch-name"
+ .icon-container
+ = custom_icon("icon_commit")
= link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: "commit-id monospace"
- if pipeline.latest?
%span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest
@@ -53,7 +55,7 @@
- if pipeline.finished_at
%p.finished-at
= icon("calendar")
- #{time_ago_with_tooltip(pipeline.finished_at, short_format: true, skip_js: true)}
+ #{time_ago_with_tooltip(pipeline.finished_at, short_format: false, skip_js: true)}
%td.pipeline-actions
.controls.hidden-xs.pull-right
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index 640abdb993f..20a85148ab5 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -1,5 +1,9 @@
-.row-content-block.build-content.middle-block
+.row-content-block.build-content.middle-block.pipeline-actions
.pull-right
+ .btn.btn-grouped.btn-white.toggle-pipeline-btn
+ %span.toggle-btn-text Hide
+ %span pipeline graph
+ %span.caret
- if can?(current_user, :update_pipeline, pipeline.project)
- if pipeline.builds.latest.failed.any?(&:retryable?)
= link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
@@ -23,6 +27,22 @@
in
= time_interval_in_words pipeline.duration
+.row-content-block.build-content.middle-block.pipeline-graph
+ .pipeline-visualization
+ %ul.stage-column-list
+ - stages = pipeline.stages_with_latest_statuses
+ - stages.each do |stage, statuses|
+ %li.stage-column
+ .stage-name
+ %a{name: stage}
+ - if stage
+ = stage.titleize
+ .builds-container
+ %ul
+ - statuses.each do |status|
+ = render "projects/#{status.to_partial_path}_pipeline", subject: status
+
+
- if pipeline.yaml_errors.present?
.bs-callout.bs-callout-danger
%h4 Found errors in your .gitlab-ci.yml:
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
new file mode 100644
index 00000000000..29f4ef8f49e
--- /dev/null
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -0,0 +1,17 @@
+%ul.content-list.pipelines
+ - if pipelines.blank?
+ %li
+ .nothing-here-block No pipelines to show
+ - else
+ .table-holder
+ %table.table.builds
+ %tbody
+ %th Status
+ %th Commit
+ - pipelines.stages.each do |stage|
+ %th.stage
+ %span.has-tooltip{ title: "#{stage.titleize}" }
+ = stage.titleize
+ %th
+ %th
+ = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, status_icon_only: true, hide_branch: true
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 8fbd89100ca..ad2eb3e504f 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -10,10 +10,9 @@
\
- if editable_diff?(diff_file)
- = edit_blob_link(@merge_request.source_project,
- @merge_request.source_branch, diff_file.new_path,
- from_merge_request_id: @merge_request.id,
- skip_visible_check: true)
+ - link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {}
+ = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
+ blob: blob, link_opts: link_opts)
= view_file_btn(diff_commit.id, diff_file.new_path, project)
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index 2d6a370b848..891b2bd9802 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -1,3 +1,4 @@
+- email = local_assigns.fetch(:email, false)
- plain = local_assigns.fetch(:plain, false)
- type = line.type
- line_code = diff_file.line_code(line) unless plain
@@ -22,4 +23,8 @@
= link_text
- else
%a{href: "##{line_code}", data: { linenumber: link_text }}
- %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }= diff_line_content(line.text, type)
+ %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }<
+ - if email
+ %pre= diff_line_content(line.text, type)
+ - else
+ = diff_line_content(line.text, type)
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
new file mode 100644
index 00000000000..584c0fa18ae
--- /dev/null
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
@@ -0,0 +1,9 @@
+%li.build
+ .build-content
+ - if subject.target_url
+ - link_to subject.target_url do
+ = render_status_with_link('commit status', subject.status)
+ = subject.name
+ - else
+ = render_status_with_link('commit status', subject.status)
+ = subject.name
diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml
index 60b45115b73..b6cb559afcb 100644
--- a/app/views/projects/issues/_head.html.haml
+++ b/app/views/projects/issues/_head.html.haml
@@ -6,6 +6,11 @@
%span
Issues
+ = nav_link(controller: :boards) do
+ = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do
+ %span
+ Board
+
- if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests)
= nav_link(controller: :merge_requests) do
= link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 598bd743676..00bd4e143df 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -20,7 +20,7 @@
.mr-compare.merge-request
%ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.commits-tab
- = link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
+ = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
Commits
%span.badge= @commits.size
- if @pipeline
@@ -52,11 +52,8 @@
$('#merge_request_assignee_id').val("#{current_user.id}").trigger("change");
e.preventDefault();
});
-
:javascript
- var merge_request
- merge_request = new MergeRequest({
- action: 'new',
- diffs_loaded: true,
- commits_loaded: true
+ var merge_request = new MergeRequest({
+ action: "#{(@show_changes_tab ? 'diffs' : 'new')}",
+ setUrl: false
});
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 269198adf91..a1313064725 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -45,20 +45,24 @@
- if @commits_count.nonzero?
%ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.notes-tab
- = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do
+ = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
Discussion
%span.badge= @merge_request.mr_and_commit_notes.user.count
%li.commits-tab
- = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
+ = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
Commits
%span.badge= @commits_count
- if @pipeline
+ %li.pipelines-tab
+ = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do
+ Pipelines
+ %span.badge= @merge_request.all_pipelines.size
%li.builds-tab
- = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#builds', action: 'builds', toggle: 'tab'} do
+ = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do
Builds
%span.badge= @statuses.size
%li.diffs-tab
- = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
+ = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
Changes
%span.badge= @merge_request.diff_size
@@ -76,6 +80,8 @@
- # This tab is always loaded via AJAX
#builds.builds.tab-pane
- # This tab is always loaded via AJAX
+ #pipelines.pipelines.tab-pane
+ - # This tab is always loaded via AJAX
#diffs.diffs.tab-pane
- # This tab is always loaded via AJAX
diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml
new file mode 100644
index 00000000000..a524936f73c
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts.html.haml
@@ -0,0 +1,29 @@
+- class_bindings = "{ |
+ 'head': line.isHead, |
+ 'origin': line.isOrigin, |
+ 'match': line.hasMatch, |
+ 'selected': line.isSelected, |
+ 'unselected': line.isUnselected }"
+
+- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
+= render "projects/merge_requests/show/mr_title"
+
+.merge-request-details.issuable-details
+ = render "projects/merge_requests/show/mr_box"
+
+= render 'shared/issuable/sidebar', issuable: @merge_request
+
+#conflicts{"v-cloak" => "true", data: { conflicts_path: conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request, format: :json),
+ resolve_conflicts_path: resolve_conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request) } }
+ .loading{"v-if" => "isLoading"}
+ %i.fa.fa-spinner.fa-spin
+
+ .nothing-here-block{"v-if" => "hasError"}
+ {{conflictsData.errorMessage}}
+
+ = render partial: "projects/merge_requests/conflicts/commit_stats"
+
+ .files-wrapper{"v-if" => "!isLoading && !hasError"}
+ = render partial: "projects/merge_requests/conflicts/parallel_view", locals: { class_bindings: class_bindings }
+ = render partial: "projects/merge_requests/conflicts/inline_view", locals: { class_bindings: class_bindings }
+ = render partial: "projects/merge_requests/conflicts/submit_form"
diff --git a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
new file mode 100644
index 00000000000..457c467fba9
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
@@ -0,0 +1,20 @@
+.content-block.oneline-block.files-changed{"v-if" => "!isLoading && !hasError"}
+ .inline-parallel-buttons
+ .btn-group
+ %a.btn{ |
+ ":class" => "{'active': !isParallel}", |
+ "@click" => "handleViewTypeChange('inline')"}
+ Inline
+ %a.btn{ |
+ ":class" => "{'active': isParallel}", |
+ "@click" => "handleViewTypeChange('parallel')"}
+ Side-by-side
+
+ .js-toggle-container
+ .commit-stat-summary
+ Showing
+ %strong.cred {{conflictsCount}} {{conflictsData.conflictsText}}
+ between
+ %strong {{conflictsData.source_branch}}
+ and
+ %strong {{conflictsData.target_branch}}
diff --git a/app/views/projects/merge_requests/conflicts/_inline_view.html.haml b/app/views/projects/merge_requests/conflicts/_inline_view.html.haml
new file mode 100644
index 00000000000..19c7da4b5e3
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/_inline_view.html.haml
@@ -0,0 +1,28 @@
+.files{"v-show" => "!isParallel"}
+ .diff-file.file-holder.conflict.inline-view{"v-for" => "file in conflictsData.files"}
+ .file-title
+ %i.fa.fa-fw{":class" => "file.iconClass"}
+ %strong {{file.filePath}}
+ .file-actions
+ %a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
+ View file @{{conflictsData.shortCommitSha}}
+
+ .diff-content.diff-wrap-lines
+ .diff-wrap-lines.code.file-content.js-syntax-highlight
+ %table
+ %tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"}
+ %template{"v-if" => "!line.isHeader"}
+ %td.diff-line-num.new_line{":class" => class_bindings}
+ %a {{line.new_line}}
+ %td.diff-line-num.old_line{":class" => class_bindings}
+ %a {{line.old_line}}
+ %td.line_content{":class" => class_bindings}
+ {{{line.richText}}}
+
+ %template{"v-if" => "line.isHeader"}
+ %td.diff-line-num.header{":class" => class_bindings}
+ %td.diff-line-num.header{":class" => class_bindings}
+ %td.line_content.header{":class" => class_bindings}
+ %strong {{{line.richText}}}
+ %button.btn{"@click" => "handleSelected(line.id, line.section)"}
+ {{line.buttonTitle}}
diff --git a/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml b/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml
new file mode 100644
index 00000000000..2e6f67c2eaf
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml
@@ -0,0 +1,27 @@
+.files{"v-show" => "isParallel"}
+ .diff-file.file-holder.conflict.parallel-view{"v-for" => "file in conflictsData.files"}
+ .file-title
+ %i.fa.fa-fw{":class" => "file.iconClass"}
+ %strong {{file.filePath}}
+ .file-actions
+ %a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
+ View file @{{conflictsData.shortCommitSha}}
+
+ .diff-content.diff-wrap-lines
+ .diff-wrap-lines.code.file-content.js-syntax-highlight
+ %table
+ %tr.line_holder.parallel{"v-for" => "section in file.parallelLines"}
+ %template{"v-for" => "line in section"}
+
+ %template{"v-if" => "line.isHeader"}
+ %td.diff-line-num.header{":class" => class_bindings}
+ %td.line_content.header{":class" => class_bindings}
+ %strong {{line.richText}}
+ %button.btn{"@click" => "handleSelected(line.id, line.section)"}
+ {{line.buttonTitle}}
+
+ %template{"v-if" => "!line.isHeader"}
+ %td.diff-line-num.old_line{":class" => class_bindings}
+ {{line.lineNumber}}
+ %td.line_content.parallel{":class" => class_bindings}
+ {{{line.richText}}}
diff --git a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
new file mode 100644
index 00000000000..78bd4133ea2
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
@@ -0,0 +1,15 @@
+.content-block.oneline-block.files-changed
+ %strong.resolved-count {{resolvedCount}}
+ of
+ %strong.total-count {{conflictsCount}}
+ conflicts have been resolved
+
+ .commit-message-container.form-group
+ .max-width-marker
+ %textarea.form-control.js-commit-message{"v-model" => "conflictsData.commitMessage"}
+ {{{conflictsData.commitMessage}}}
+
+ %button{type: "button", class: "btn btn-success js-submit-button", ":disabled" => "!readyToCommit", "@click" => "commit()"}
+ %span {{commitButtonText}}
+
+ = link_to "Cancel", namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request), class: "btn btn-cancel"
diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml
index 81de60f116c..808ef7fed27 100644
--- a/app/views/projects/merge_requests/show/_builds.html.haml
+++ b/app/views/projects/merge_requests/show/_builds.html.haml
@@ -1,2 +1 @@
= render "projects/commit/pipeline", pipeline: @pipeline, link_to_commit: true
-
diff --git a/app/views/projects/merge_requests/show/_pipelines.html.haml b/app/views/projects/merge_requests/show/_pipelines.html.haml
new file mode 100644
index 00000000000..afe3f3430c6
--- /dev/null
+++ b/app/views/projects/merge_requests/show/_pipelines.html.haml
@@ -0,0 +1 @@
+= render "projects/commit/pipelines_list", pipelines: @pipelines, link_to_commit: true
diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml
index 19b5d0ff066..7794d6d7df2 100644
--- a/app/views/projects/merge_requests/widget/_merged.html.haml
+++ b/app/views/projects/merge_requests/widget/_merged.html.haml
@@ -6,7 +6,7 @@
- if @merge_request.merge_event
by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)}
#{time_ago_with_tooltip(@merge_request.merge_event.created_at)}
- - if !@merge_request.source_branch_exists? || (params[:delete_source] == 'true')
+ - if !@merge_request.source_branch_exists? || params[:deleted_source_branch]
%p
The changes were merged into
#{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index dc18f715f25..6f5ee5f16c5 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -1,6 +1,12 @@
.mr-state-widget
= render 'projects/merge_requests/widget/heading'
.mr-widget-body
+ -# After conflicts are resolved, the user is redirected back to the MR page.
+ -# There is a short window before background workers run and GitLab processes
+ -# the new push and commits, during which it will think the conflicts still exist.
+ -# We send this param to get the widget to treat the MR as having no more conflicts.
+ - resolved_conflicts = params[:resolved_conflicts]
+
- if @project.archived?
= render 'projects/merge_requests/widget/open/archived'
- elsif @merge_request.commits.blank?
@@ -9,7 +15,7 @@
= render 'projects/merge_requests/widget/open/missing_branch'
- elsif @merge_request.unchecked?
= render 'projects/merge_requests/widget/open/check'
- - elsif @merge_request.cannot_be_merged?
+ - elsif @merge_request.cannot_be_merged? && !resolved_conflicts
= render 'projects/merge_requests/widget/open/conflicts'
- elsif @merge_request.work_in_progress?
= render 'projects/merge_requests/widget/open/wip'
@@ -19,7 +25,7 @@
= render 'projects/merge_requests/widget/open/not_allowed'
- elsif !@merge_request.mergeable_ci_state? && @pipeline && @pipeline.failed?
= render 'projects/merge_requests/widget/open/build_failed'
- - elsif @merge_request.can_be_merged?
+ - elsif @merge_request.can_be_merged? || resolved_conflicts
= render 'projects/merge_requests/widget/open/accept'
- if mr_closes_issues.present?
diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index d9efe81701f..ea618263a4a 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -23,7 +23,8 @@
preparing: "{{status}} build",
normal: "Build {{status}}"
},
- builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}"
+ builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
+ pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}"
};
if (typeof merge_request_widget !== 'undefined') {
diff --git a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
index f000cc38a65..af3096f04d9 100644
--- a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
@@ -3,7 +3,18 @@
This merge request contains merge conflicts
%p
- Please resolve these conflicts or
+ Please
+ - if @merge_request.conflicts_can_be_resolved_by?(current_user)
+ - if @merge_request.conflicts_can_be_resolved_in_ui?
+ = link_to "resolve these conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+ - else
+ %span.has-tooltip{title: "These conflicts cannot be resolved through GitLab"}
+ resolve these conflicts locally
+ - else
+ resolve these conflicts
+
+ or
+
- if @merge_request.can_be_merged_via_command_line_by?(current_user)
#{link_to "merge this request manually", "#modal_merge_info", class: "how_to_merge_link vlink", "data-toggle" => "modal"}.
- else
diff --git a/app/views/shared/icons/_icon_fork.svg b/app/views/shared/icons/_icon_fork.svg
index a21f8f3a951..fc970e4ce50 100644
--- a/app/views/shared/icons/_icon_fork.svg
+++ b/app/views/shared/icons/_icon_fork.svg
@@ -1,3 +1,3 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
+<svg xmlns="http://www.w3.org/2000/svg" width="30" height="40" viewBox="5 0 30 40">
<path fill="#7E7E7E" fill-rule="evenodd" d="M22,29.5351288 L22,22.7193602 C26.1888699,21.5098039 29.3985457,16.802989 29.3985457,16.802989 C29.740988,16.3567547 30,15.5559546 30,15.0081969 L30,10.4648712 C31.1956027,9.77325238 32,8.48056471 32,7 C32,4.790861 30.209139,3 28,3 C25.790861,3 24,4.790861 24,7 C24,8.48056471 24.8043973,9.77325238 26,10.4648712 L26,14.7083871 C26,14.8784435 25.9055559,15.0987329 25.7890533,15.2104147 C25.7890533,15.2104147 24.5373893,16.4126202 23.9488702,16.9515733 C22.5015398,18.2770075 21.1191354,19 20.090554,19 C19.0477772,19 17.6172728,18.2608988 16.1128852,16.9142923 C15.5030182,16.3683886 14.3672121,15.3403307 14.3672121,15.3403307 C14.1659605,15.1583364 14.0000086,14.7846305 14.0000192,14.5088473 C14.0000192,14.5088473 14.0000932,12.7539451 14.0001308,10.4647956 C15.1956614,9.77315812 16,8.48051074 16,7 C16,4.790861 14.209139,3 12,3 C9.790861,3 8,4.790861 8,7 C8,8.48056471 8.80439726,9.77325238 10,10.4648712 L10,15.0081969 C10,15.5446944 10.2736352,16.3534183 10.6111812,16.7893819 C10.6111812,16.7893819 13.8599776,21.3779363 18,22.6668724 L18,29.5351288 C16.8043973,30.2267476 16,31.5194353 16,33 C16,35.209139 17.790861,37 20,37 C22.209139,37 24,35.209139 24,33 C24,31.5194353 23.1956027,30.2267476 22,29.5351288 Z M14,7 C14,5.8954305 13.1045695,5 12,5 C10.8954305,5 10,5.8954305 10,7 C10,8.1045695 10.8954305,9 12,9 C13.1045695,9 14,8.1045695 14,7 Z M30,7 C30,5.8954305 29.1045695,5 28,5 C26.8954305,5 26,5.8954305 26,7 C26,8.1045695 26.8954305,9 28,9 C29.1045695,9 30,8.1045695 30,7 Z M22,33 C22,31.8954305 21.1045695,31 20,31 C18.8954305,31 18,31.8954305 18,33 C18,34.1045695 18.8954305,35 20,35 C21.1045695,35 22,34.1045695 22,33 Z"/>
</svg>
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index c957cd84479..4f8ea7e7cef 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -27,7 +27,17 @@
= render "shared/issuable/label_dropdown"
.pull-right
- = render 'shared/sort_dropdown'
+ - if controller.controller_name == 'boards' && can?(current_user, :admin_list, @project)
+ .dropdown
+ %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, project_id: @project.try(:id) } }
+ Create new list
+ .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
+ = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" }
+ - if can?(current_user, :admin_label, @project)
+ = render partial: "shared/issuable/label_page_create"
+ = dropdown_loading
+ - else
+ = render 'shared/sort_dropdown'
- if controller.controller_name == 'issues'
.issues_bulk_update.hide
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 210b43c7e0b..9e2e096d5f9 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -42,7 +42,7 @@
- if can_add_template?(issuable)
%p.help-block
Add
- = link_to "issuable templates", help_page_path('workflow/description_templates')
+ = link_to "description templates", help_page_path('user/project/description_templates')
to help your contributors communicate effectively!
.form-group.detail-page-description
diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml
index 4e280c371ac..a76b7baf918 100644
--- a/app/views/shared/issuable/_label_page_default.html.haml
+++ b/app/views/shared/issuable/_label_page_default.html.haml
@@ -2,8 +2,13 @@
- show_create = local_assigns.fetch(:show_create, true)
- show_footer = local_assigns.fetch(:show_footer, true)
- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels')
+- show_boards_content = local_assigns.fetch(:show_boards_content, false)
.dropdown-page-one
= dropdown_title(title)
+ - if show_boards_content
+ .issue-board-dropdown-content
+ %p
+ Each label that exists in your issue tracker can have its own dedicated list. Select a label below to add a list to your Board and it will automatically be populated with issues that have that label. To create a list for a label that doesn't exist yet, simply create the label below.
= dropdown_filter(filter_placeholder)
= dropdown_content
- if @project && show_footer
@@ -12,7 +17,7 @@
- if can?(current_user, :admin_label, @project)
%li
%a.dropdown-toggle-page{href: "#"}
- Create new
+ Create new label
%li
= link_to namespace_project_labels_path(@project.namespace, @project), :"data-is-link" => true do
- if show_create && @project && can?(current_user, :admin_label, @project)
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 281ec728e41..66c309644a7 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -20,11 +20,11 @@
- if forks
%span
= icon('code-fork')
- = project.forks_count
+ = number_with_delimiter(project.forks_count)
- if stars
%span
= icon('star')
- = project.star_count
+ = number_with_delimiter(project.star_count)
%span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)}
= visibility_level_icon(project.visibility_level, fw: true)
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
index cbb8dfb7829..8f7b42eb351 100644
--- a/app/views/u2f/_register.html.haml
+++ b/app/views/u2f/_register.html.haml
@@ -28,10 +28,15 @@
%script#js-register-u2f-registered{ type: "text/template" }
%div.row.append-bottom-10
- %p Your device was successfully set up! Click this button to register with the GitLab server.
- = form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do
- = hidden_field_tag :device_response, nil, class: 'form-control', required: true, id: "js-device-response"
- = submit_tag "Register U2F Device", class: "btn btn-success"
+ .col-md-12
+ %p Your device was successfully set up! Give it a name and register it with the GitLab server.
+ = form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do
+ .row.append-bottom-10
+ .col-md-3
+ = text_field_tag 'u2f_registration[name]', nil, class: 'form-control', placeholder: "Pick a name"
+ .col-md-3
+ = hidden_field_tag 'u2f_registration[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
+ = submit_tag "Register U2F Device", class: "btn btn-success"
:javascript
var u2fRegister = new U2FRegister($("#js-register-u2f"), gon.u2f);
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index d69d6037053..61ed1c38ac4 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -5,6 +5,10 @@ class RepositoryForkWorker
sidekiq_options queue: :gitlab_shell
def perform(project_id, forked_from_repository_storage_path, source_path, target_path)
+ Gitlab::Metrics.add_event(:fork_repository,
+ source_path: source_path,
+ target_path: target_path)
+
project = Project.find_by_id(project_id)
unless project.present?
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 7d819fe78f8..e6701078f71 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -10,6 +10,10 @@ class RepositoryImportWorker
@project = Project.find(project_id)
@current_user = @project.creator
+ Gitlab::Metrics.add_event(:import_repository,
+ import_url: @project.import_url,
+ path: @project.path_with_namespace)
+
result = Projects::ImportService.new(project, current_user).execute
if result[:status] == :error
diff --git a/config/application.rb b/config/application.rb
index 4a9ed41cbf8..0c136623477 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -85,6 +85,8 @@ module Gitlab
config.assets.precompile << "users/users_bundle.js"
config.assets.precompile << "network/network_bundle.js"
config.assets.precompile << "profile/profile_bundle.js"
+ config.assets.precompile << "boards/boards_bundle.js"
+ config.assets.precompile << "boards/test_utils/simulate_drag.js"
config.assets.precompile << "lib/utils/*.js"
config.assets.precompile << "lib/*.js"
config.assets.precompile << "u2f.js"
diff --git a/config/routes.rb b/config/routes.rb
index 63a8827a6a2..5c48bf233d6 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -375,6 +375,8 @@ Rails.application.routes.draw do
patch :skip
end
end
+
+ resources :u2f_registrations, only: [:destroy]
end
end
@@ -727,7 +729,9 @@ Rails.application.routes.draw do
member do
get :commits
get :diffs
+ get :conflicts
get :builds
+ get :pipelines
get :merge_check
post :merge
post :cancel_merge_when_build_succeeds
@@ -736,6 +740,7 @@ Rails.application.routes.draw do
post :toggle_award_emoji
post :remove_wip
get :diff_for_path
+ post :resolve_conflicts
end
collection do
@@ -856,6 +861,20 @@ Rails.application.routes.draw do
end
end
+ resource :board, only: [:show] do
+ scope module: :boards do
+ resources :issues, only: [:update]
+
+ resources :lists, only: [:index, :create, :update, :destroy] do
+ collection do
+ post :generate
+ end
+
+ resources :issues, only: [:index]
+ end
+ end
+ end
+
resources :todos, only: [:create]
resources :uploads, only: [:create] do
diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb
index 6441a036e75..069d9dd6226 100644
--- a/db/fixtures/development/14_builds.rb
+++ b/db/fixtures/development/14_builds.rb
@@ -1,9 +1,8 @@
class Gitlab::Seeder::Builds
- STAGES = %w[build notify_build test notify_test deploy notify_deploy]
+ STAGES = %w[build test deploy notify]
BUILDS = [
{ name: 'build:linux', stage: 'build', status: :success },
{ name: 'build:osx', stage: 'build', status: :success },
- { name: 'slack post build', stage: 'notify_build', status: :success },
{ name: 'rspec:linux', stage: 'test', status: :success },
{ name: 'rspec:windows', stage: 'test', status: :success },
{ name: 'rspec:windows', stage: 'test', status: :success },
@@ -12,9 +11,9 @@ class Gitlab::Seeder::Builds
{ name: 'spinach:osx', stage: 'test', status: :canceled },
{ name: 'cucumber:linux', stage: 'test', status: :running },
{ name: 'cucumber:osx', stage: 'test', status: :failed },
- { name: 'slack post test', stage: 'notify_test', status: :success },
{ name: 'staging', stage: 'deploy', environment: 'staging', status: :success },
- { name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :success },
+ { name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :skipped },
+ { name: 'slack', stage: 'notify', when: 'manual', status: :created },
]
def initialize(project)
@@ -25,25 +24,45 @@ class Gitlab::Seeder::Builds
pipelines.each do |pipeline|
begin
BUILDS.each { |opts| build_create!(pipeline, opts) }
- commit_status_create!(pipeline, name: 'jenkins', status: :success)
-
+ commit_status_create!(pipeline, name: 'jenkins', stage: 'test', status: :success)
print '.'
rescue ActiveRecord::RecordInvalid
print 'F'
+ ensure
+ pipeline.build_updated
end
end
end
def pipelines
- commits = @project.repository.commits('master', limit: 5)
- commits_sha = commits.map { |commit| commit.raw.id }
- commits_sha.map do |sha|
- @project.ensure_pipeline(sha, 'master')
- end
+ master_pipelines + merge_request_pipelines
+ end
+
+ def master_pipelines
+ create_pipelines_for(@project, 'master')
rescue
[]
end
+ def merge_request_pipelines
+ @project.merge_requests.last(5).map do |merge_request|
+ create_pipelines(merge_request.source_project, merge_request.source_branch, merge_request.commits.last(5))
+ end.flatten
+ rescue
+ []
+ end
+
+ def create_pipelines_for(project, ref)
+ commits = project.repository.commits(ref, limit: 5)
+ create_pipelines(project, ref, commits)
+ end
+
+ def create_pipelines(project, ref, commits)
+ commits.map do |commit|
+ project.pipelines.create(sha: commit.id, ref: ref)
+ end
+ end
+
def build_create!(pipeline, opts = {})
attributes = build_attributes_for(pipeline, opts)
diff --git a/db/migrate/20160727191041_create_boards.rb b/db/migrate/20160727191041_create_boards.rb
new file mode 100644
index 00000000000..56afbd4e030
--- /dev/null
+++ b/db/migrate/20160727191041_create_boards.rb
@@ -0,0 +1,13 @@
+class CreateBoards < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :boards do |t|
+ t.references :project, index: true, foreign_key: true, null: false
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160727193336_create_lists.rb b/db/migrate/20160727193336_create_lists.rb
new file mode 100644
index 00000000000..61d501215f2
--- /dev/null
+++ b/db/migrate/20160727193336_create_lists.rb
@@ -0,0 +1,16 @@
+class CreateLists < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :lists do |t|
+ t.references :board, index: true, foreign_key: true, null: false
+ t.references :label, index: true, foreign_key: true
+ t.integer :list_type, null: false, default: 1
+ t.integer :position
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb b/db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb
new file mode 100644
index 00000000000..baf2e70b127
--- /dev/null
+++ b/db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb
@@ -0,0 +1,15 @@
+class AddUniqueIndexToListsLabelId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :lists, [:board_id, :label_id], unique: true
+ end
+
+ def down
+ remove_index :lists, column: [:board_id, :label_id] if index_exists?(:lists, [:board_id, :label_id], unique: true)
+ end
+end
diff --git a/db/migrate/20160816161312_add_column_name_to_u2f_registrations.rb b/db/migrate/20160816161312_add_column_name_to_u2f_registrations.rb
new file mode 100644
index 00000000000..7152bd04331
--- /dev/null
+++ b/db/migrate/20160816161312_add_column_name_to_u2f_registrations.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddColumnNameToU2fRegistrations < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ add_column :u2f_registrations, :name, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 445f484a8c7..fa56db332b3 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160810142633) do
+ActiveRecord::Schema.define(version: 20160816161312) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -117,6 +117,14 @@ ActiveRecord::Schema.define(version: 20160810142633) do
add_index "award_emoji", ["user_id", "name"], name: "index_award_emoji_on_user_id_and_name", using: :btree
add_index "award_emoji", ["user_id"], name: "index_award_emoji_on_user_id", using: :btree
+ create_table "boards", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "boards", ["project_id"], name: "index_boards_on_project_id", using: :btree
+
create_table "broadcast_messages", force: :cascade do |t|
t.text "message", null: false
t.datetime "starts_at"
@@ -533,6 +541,19 @@ ActiveRecord::Schema.define(version: 20160810142633) do
add_index "lfs_objects_projects", ["project_id"], name: "index_lfs_objects_projects_on_project_id", using: :btree
+ create_table "lists", force: :cascade do |t|
+ t.integer "board_id", null: false
+ t.integer "label_id"
+ t.integer "list_type", default: 1, null: false
+ t.integer "position"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "lists", ["board_id", "label_id"], name: "index_lists_on_board_id_and_label_id", unique: true, using: :btree
+ add_index "lists", ["board_id"], name: "index_lists_on_board_id", using: :btree
+ add_index "lists", ["label_id"], name: "index_lists_on_label_id", using: :btree
+
create_table "members", force: :cascade do |t|
t.integer "access_level", null: false
t.integer "source_id", null: false
@@ -995,6 +1016,7 @@ ActiveRecord::Schema.define(version: 20160810142633) do
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.string "name"
end
add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree
@@ -1116,6 +1138,9 @@ ActiveRecord::Schema.define(version: 20160810142633) do
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
+ add_foreign_key "boards", "projects"
+ add_foreign_key "lists", "boards"
+ add_foreign_key "lists", "labels"
add_foreign_key "personal_access_tokens", "users"
add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
add_foreign_key "protected_branch_push_access_levels", "protected_branches"
diff --git a/doc/api/README.md b/doc/api/README.md
index f3117815c7c..3e79cce0120 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -26,6 +26,7 @@ following locations:
- [Open source license templates](licenses.md)
- [Namespaces](namespaces.md)
- [Notes](notes.md) (comments)
+- [Pipelines](pipelines.md)
- [Projects](projects.md) including setting Webhooks
- [Project Access Requests](access_requests.md)
- [Project Members](members.md)
diff --git a/doc/api/builds.md b/doc/api/builds.md
index 8864df03c98..dce666445d0 100644
--- a/doc/api/builds.md
+++ b/doc/api/builds.md
@@ -532,3 +532,49 @@ Example response:
"user": null
}
```
+
+## Play a build
+
+Triggers a manual action to start a build.
+
+```
+POST /projects/:id/builds/:build_id/play
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `build_id` | integer | yes | The ID of a build |
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/play"
+```
+
+Example of response
+
+```json
+{
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "artifacts_file": null,
+ "finished_at": null,
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": null,
+ "status": "started",
+ "tag": false,
+ "user": null
+}
+```
diff --git a/doc/api/deployments.md b/doc/api/deployments.md
new file mode 100644
index 00000000000..417962de82d
--- /dev/null
+++ b/doc/api/deployments.md
@@ -0,0 +1,218 @@
+# Deployments API
+
+## List project deployments
+
+Get a list of deployments in a project.
+
+```
+GET /projects/:id/deployments
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/deployments"
+```
+
+Example of response
+
+```json
+[
+ {
+ "created_at": "2016-08-11T07:36:40.222Z",
+ "deployable": {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2016-08-11T09:36:01.000+02:00",
+ "id": "99d03678b90d914dbb1b109132516d71a4a03ea8",
+ "message": "Merge branch 'new-title' into 'master'\r\n\r\nUpdate README\r\n\r\n\r\n\r\nSee merge request !1",
+ "short_id": "99d03678",
+ "title": "Merge branch 'new-title' into 'master'\r"
+ },
+ "coverage": null,
+ "created_at": "2016-08-11T07:36:27.357Z",
+ "finished_at": "2016-08-11T07:36:39.851Z",
+ "id": 657,
+ "name": "deploy",
+ "ref": "master",
+ "runner": null,
+ "stage": "deploy",
+ "started_at": null,
+ "status": "success",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2016-08-11T07:09:20.351Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "location": null,
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://localhost:3000/u/root",
+ "website_url": ""
+ }
+ },
+ "environment": {
+ "external_url": "https://about.gitlab.com",
+ "id": 9,
+ "name": "production"
+ },
+ "id": 41,
+ "iid": 1,
+ "ref": "master",
+ "sha": "99d03678b90d914dbb1b109132516d71a4a03ea8",
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "id": 1,
+ "name": "Administrator",
+ "state": "active",
+ "username": "root",
+ "web_url": "http://localhost:3000/u/root"
+ }
+ },
+ {
+ "created_at": "2016-08-11T11:32:35.444Z",
+ "deployable": {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2016-08-11T13:28:26.000+02:00",
+ "id": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "message": "Merge branch 'rename-readme' into 'master'\r\n\r\nRename README\r\n\r\n\r\n\r\nSee merge request !2",
+ "short_id": "a91957a8",
+ "title": "Merge branch 'rename-readme' into 'master'\r"
+ },
+ "coverage": null,
+ "created_at": "2016-08-11T11:32:24.456Z",
+ "finished_at": "2016-08-11T11:32:35.145Z",
+ "id": 664,
+ "name": "deploy",
+ "ref": "master",
+ "runner": null,
+ "stage": "deploy",
+ "started_at": null,
+ "status": "success",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2016-08-11T07:09:20.351Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "location": null,
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://localhost:3000/u/root",
+ "website_url": ""
+ }
+ },
+ "environment": {
+ "external_url": "https://about.gitlab.com",
+ "id": 9,
+ "name": "production"
+ },
+ "id": 42,
+ "iid": 2,
+ "ref": "master",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "id": 1,
+ "name": "Administrator",
+ "state": "active",
+ "username": "root",
+ "web_url": "http://localhost:3000/u/root"
+ }
+ }
+]
+```
+
+## Get a specific deployment
+
+```
+GET /projects/:id/deployments/:deployment_id
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `deployment_id` | integer | yes | The ID of the deployment |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/deployments/1"
+```
+
+Example of response
+
+```json
+{
+ "id": 42,
+ "iid": 2,
+ "ref": "master",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "created_at": "2016-08-11T11:32:35.444Z",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://localhost:3000/u/root"
+ },
+ "environment": {
+ "id": 9,
+ "name": "production",
+ "external_url": "https://about.gitlab.com"
+ },
+ "deployable": {
+ "id": 664,
+ "status": "success",
+ "stage": "deploy",
+ "name": "deploy",
+ "ref": "master",
+ "tag": false,
+ "coverage": null,
+ "created_at": "2016-08-11T11:32:24.456Z",
+ "started_at": null,
+ "finished_at": "2016-08-11T11:32:35.145Z",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://localhost:3000/u/root",
+ "created_at": "2016-08-11T07:09:20.351Z",
+ "is_admin": true,
+ "bio": null,
+ "location": null,
+ "skype": "",
+ "linkedin": "",
+ "twitter": "",
+ "website_url": ""
+ },
+ "commit": {
+ "id": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "short_id": "a91957a8",
+ "title": "Merge branch 'rename-readme' into 'master'\r",
+ "author_name": "Administrator",
+ "author_email": "admin@example.com",
+ "created_at": "2016-08-11T13:28:26.000+02:00",
+ "message": "Merge branch 'rename-readme' into 'master'\r\n\r\nRename README\r\n\r\n\r\n\r\nSee merge request !2"
+ },
+ "runner": null
+ }
+}
+```
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
new file mode 100644
index 00000000000..847408a7f61
--- /dev/null
+++ b/doc/api/pipelines.md
@@ -0,0 +1,207 @@
+# Pipelines API
+
+## List project pipelines
+
+> [Introduced][ce-5837] in GitLab 8.11
+
+```
+GET /projects/:id/pipelines
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines"
+```
+
+Example of response
+
+```json
+[
+ {
+ "id": 47,
+ "status": "pending",
+ "ref": "new-pipeline",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "tag": false,
+ "yaml_errors": null,
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://localhost:3000/u/root"
+ },
+ "created_at": "2016-08-16T10:23:19.007Z",
+ "updated_at": "2016-08-16T10:23:19.216Z",
+ "started_at": null,
+ "finished_at": null,
+ "committed_at": null,
+ "duration": null
+ },
+ {
+ "id": 48,
+ "status": "pending",
+ "ref": "new-pipeline",
+ "sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a",
+ "before_sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a",
+ "tag": false,
+ "yaml_errors": null,
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://localhost:3000/u/root"
+ },
+ "created_at": "2016-08-16T10:23:21.184Z",
+ "updated_at": "2016-08-16T10:23:21.314Z",
+ "started_at": null,
+ "finished_at": null,
+ "committed_at": null,
+ "duration": null
+ }
+]
+```
+
+## Get a single pipeline
+
+> [Introduced][ce-5837] in GitLab 8.11
+
+```
+GET /projects/:id/pipelines/:pipeline_id
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `pipeline_id` | integer | yes | The ID of a pipeline |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipeline/46"
+```
+
+Example of response
+
+```json
+{
+ "id": 46,
+ "status": "success",
+ "ref": "master",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "tag": false,
+ "yaml_errors": null,
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://localhost:3000/u/root"
+ },
+ "created_at": "2016-08-11T11:28:34.085Z",
+ "updated_at": "2016-08-11T11:32:35.169Z",
+ "started_at": null,
+ "finished_at": "2016-08-11T11:32:35.145Z",
+ "committed_at": null,
+ "duration": null
+}
+```
+
+## Retry failed builds in a pipeline
+
+> [Introduced][ce-5837] in GitLab 8.11
+
+```
+POST /projects/:id/pipelines/:pipeline_id/retry
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `pipeline_id` | integer | yes | The ID of a pipeline |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines/46/retry"
+```
+
+Response:
+
+```json
+{
+ "id": 46,
+ "status": "pending",
+ "ref": "master",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "tag": false,
+ "yaml_errors": null,
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://localhost:3000/u/root"
+ },
+ "created_at": "2016-08-11T11:28:34.085Z",
+ "updated_at": "2016-08-11T11:32:35.169Z",
+ "started_at": null,
+ "finished_at": "2016-08-11T11:32:35.145Z",
+ "committed_at": null,
+ "duration": null
+}
+```
+
+## Cancel a pipelines builds
+
+> [Introduced][ce-5837] in GitLab 8.11
+
+```
+POST /projects/:id/pipelines/:pipeline_id/cancel
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `pipeline_id` | integer | yes | The ID of a pipeline |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines/46/cancel"
+```
+
+Response:
+
+```json
+{
+ "id": 46,
+ "status": "canceled",
+ "ref": "master",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "tag": false,
+ "yaml_errors": null,
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://localhost:3000/u/root"
+ },
+ "created_at": "2016-08-11T11:28:34.085Z",
+ "updated_at": "2016-08-11T11:32:35.169Z",
+ "started_at": null,
+ "finished_at": "2016-08-11T11:32:35.145Z",
+ "committed_at": null,
+ "duration": null
+}
+```
+
+[ce-5837]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5837
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 01d71088543..e7850aa2c9d 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -353,7 +353,7 @@ job_name:
| except | no | Defines a list of git refs for which build is not created |
| tags | no | Defines a list of tags which are used to select Runner |
| allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status |
-| when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` |
+| when | no | Define when to run build. Can be `on_success`, `on_failure`, `always` or `manual` |
| dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them|
| artifacts | no | Define list of build artifacts |
| cache | no | Define list of files that should be cached between subsequent runs |
diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md
index edd6c59138f..7f08188bd65 100644
--- a/doc/legal/corporate_contributor_license_agreement.md
+++ b/doc/legal/corporate_contributor_license_agreement.md
@@ -16,7 +16,7 @@ Subject to the terms and conditions of this Agreement, You hereby grant to GitLa
Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
-4. You represent that You are legally entitled to grant the above license. You represent further that each employee of the Corporation is authorized to submit Contributions on behalf of the Corporation, but excluding employees that are designated in writing by You as "Not authorized to submit Contributions on behalf of [name of corporation here]."
+4. You represent that You are legally entitled to grant the above license. You represent further that each of Your employees is authorized to submit Contributions on Your behalf, but excluding employees that are designated in writing by You as "Not authorized to submit Contributions on behalf of [name of Your corporation here]." Such designations of exclusion for unauthorized employees are to be submitted via email to legal@gitlab.com.
5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others).
@@ -24,6 +24,6 @@ Subject to the terms and conditions of this Agreement, You hereby grant to GitLa
7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
-8. It is your responsibility to notify GitLab B.V. when any change is required to the designation of employees not authorized to submit Contributions on behalf of the Corporation, or to the Corporation's Point of Contact with GitLab B.V..
+8. It is Your responsibility to notify GitLab.com when any change is required to the list of designated employees excluded from submitting Contributions on Your behalf per Section 4. Such notification should be sent via email to legal@gitlab.com.
This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
diff --git a/doc/monitoring/performance/influxdb_schema.md b/doc/monitoring/performance/influxdb_schema.md
index 41861860b6d..eff0e29f58d 100644
--- a/doc/monitoring/performance/influxdb_schema.md
+++ b/doc/monitoring/performance/influxdb_schema.md
@@ -9,6 +9,7 @@ The following measurements are currently stored in InfluxDB:
- `PROCESS_object_counts`
- `PROCESS_transactions`
- `PROCESS_views`
+- `events`
Here, `PROCESS` is replaced with either `rails` or `sidekiq` depending on the
process type. In all series, any form of duration is stored in milliseconds.
@@ -78,6 +79,14 @@ following value fields are available:
The `action` tag contains the action name of the transaction that rendered the
view.
+## events
+
+This measurement is used to store generic events such as the number of Git
+pushes, Emails sent, etc. Each point in this measurement has a single value
+field called `count`. The value of this field is simply set to `1`. Each point
+also has at least one tag: `event`. This tag's value is set to the event name.
+Depending on the event type additional tags may be available as well.
+
---
Read more on:
diff --git a/doc/user/project/description_templates.md b/doc/user/project/description_templates.md
new file mode 100644
index 00000000000..ea7496af089
--- /dev/null
+++ b/doc/user/project/description_templates.md
@@ -0,0 +1,42 @@
+# Description templates
+
+>[Introduced][ce-4981] in GitLab 8.11.
+
+Description templates allow you to define context-specific templates for issue
+and merge request description fields for your project.
+
+## Overview
+
+By using the description templates, users that create a new issue or merge
+request can select a description template to help them communicate with other
+contributors effectively.
+
+Every GitLab project can define its own set of description templates as they
+are added to the root directory of a GitLab project's repository.
+
+Description templates must be written in [Markdown](../markdown.md) and stored
+in your project's repository under a directory named `.gitlab`. Only the
+templates of the default branch will be taken into account.
+
+## Creating issue templates
+
+Create a new Markdown (`.md`) file inside the `.gitlab/issue_templates/`
+directory in your repository. Commit and push to your default branch.
+
+## Creating merge request templates
+
+Similarly to issue templates, create a new Markdown (`.md`) file inside the
+`.gitlab/merge_request_templates/` directory in your repository. Commit and
+push to your default branch.
+
+## Using the templates
+
+Let's take for example that you've created the file `.gitlab/issue_templates/Bug.md`.
+This will enable the `Bug` dropdown option when creating or editing issues. When
+`Bug` is selected, the content from the `Bug.md` template file will be copied
+to the issue description field. The 'Reset template' button will discard any
+changes you made after picking the template and return it to its initial status.
+
+![Description templates](img/description_templates.png)
+
+[ce-4981]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4981
diff --git a/doc/user/project/img/description_templates.png b/doc/user/project/img/description_templates.png
new file mode 100644
index 00000000000..c41cc77a94c
--- /dev/null
+++ b/doc/user/project/img/description_templates.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/conflict_section.png b/doc/user/project/merge_requests/img/conflict_section.png
new file mode 100644
index 00000000000..842e50b14b2
--- /dev/null
+++ b/doc/user/project/merge_requests/img/conflict_section.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_request_widget.png b/doc/user/project/merge_requests/img/merge_request_widget.png
new file mode 100644
index 00000000000..ffb96b17b07
--- /dev/null
+++ b/doc/user/project/merge_requests/img/merge_request_widget.png
Binary files differ
diff --git a/doc/user/project/merge_requests/resolve_conflicts.md b/doc/user/project/merge_requests/resolve_conflicts.md
new file mode 100644
index 00000000000..44b76ffc8e6
--- /dev/null
+++ b/doc/user/project/merge_requests/resolve_conflicts.md
@@ -0,0 +1,41 @@
+# Merge conflict resolution
+
+> [Introduced][ce-5479] in GitLab 8.11.
+
+When a merge request has conflicts, GitLab may provide the option to resolve
+those conflicts in the GitLab UI. (See
+[conflicts available for resolution](#conflicts-available-for-resolution) for
+more information on when this is available.) If this is an option, you will see
+a **resolve these conflicts** link in the merge request widget:
+
+![Merge request widget](img/merge_request_widget.png)
+
+Clicking this will show a list of files with conflicts, with conflict sections
+highlighted:
+
+![Conflict section](img/conflict_section.png)
+
+Once all conflicts have been marked as using 'ours' or 'theirs', the conflict
+can be resolved. This will perform a merge of the target branch of the merge
+request into the source branch, resolving the conflicts using the options
+chosen. If the source branch is `feature` and the target branch is `master`,
+this is similar to performing `git checkout feature; git merge master` locally.
+
+## Conflicts available for resolution
+
+GitLab allows resolving conflicts in a file where all of the below are true:
+
+- The file is text, not binary
+- The file does not already contain conflict markers
+- The file, with conflict markers added, is not over 200 KB in size
+- The file exists under the same path in both branches
+
+If any file with conflicts in that merge request does not meet all of these
+criteria, the conflicts for that merge request cannot be resolved in the UI.
+
+Additionally, GitLab does not detect conflicts in renames away from a path. For
+example, this will not create a conflict: on branch `a`, doing `git mv file1
+file2`; on branch `b`, doing `git mv file1 file3`. Instead, both files will be
+present in the branch after the merge request is merged.
+
+[ce-5479]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5479
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 993349e5b46..3055411c484 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -2,6 +2,7 @@
- [Authorization for merge requests](authorization_for_merge_requests.md)
- [Change your time zone](timezone.md)
+- [Description templates](../user/project/description_templates.md)
- [Feature branch workflow](workflow.md)
- [GitLab Flow](gitlab_flow.md)
- [Groups](groups.md)
@@ -17,7 +18,6 @@
- [Share projects with other groups](share_projects_with_other_groups.md)
- [Web Editor](web_editor.md)
- [Releases](releases.md)
-- [Issuable Templates](issuable_templates.md)
- [Milestones](milestones.md)
- [Merge Requests](merge_requests.md)
- [Revert changes](revert_changes.md)
diff --git a/doc/workflow/description_templates.md b/doc/workflow/description_templates.md
deleted file mode 100644
index 9514564af02..00000000000
--- a/doc/workflow/description_templates.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# Description templates
-
-Description templates allow you to define context-specific templates for issue and merge request description fields for your project. When in use, users that create a new issue or merge request can select a description template to help them communicate with other contributors effectively.
-
-Every GitLab project can define its own set of description templates as they are added to the root directory of a GitLab project's repository.
-
-Description templates are written in markdown _(`.md`)_ and stored in your projects repository under the `/.gitlab/issue_templates/` and `/.gitlab/merge_request_templates/` directories.
-
-![Description templates](img/description_templates.png)
-
-_Example:_
-`/.gitlab/issue_templates/bug.md` will enable the `bug` dropdown option for new issues. When `bug` is selected, the content from the `bug.md` template file will be copied to the issue description field.
diff --git a/doc/workflow/img/description_templates.png b/doc/workflow/img/description_templates.png
deleted file mode 100644
index af2e9403826..00000000000
--- a/doc/workflow/img/description_templates.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index b4a9c2f3d3e..1b49a5c385f 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -67,7 +67,7 @@ In all of the below cases, the notification will be sent to:
- Participants:
- the author and assignee of the issue/merge request
- authors of comments on the issue/merge request
- - anyone mentioned by `@username` in the issue/merge request description
+ - anyone mentioned by `@username` in the issue/merge request title or description
- anyone mentioned by `@username` in any of the comments on the issue/merge request
...with notification level "Participating" or higher
@@ -89,6 +89,11 @@ In all of the below cases, the notification will be sent to:
| Merge merge request | |
| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher |
+
+In addition, if the title or description of an Issue or Merge Request is
+changed, notifications will be sent to any **new** mentions by `@username` as
+if they had been mentioned in the original text.
+
You won't receive notifications for Issues, Merge Requests or Milestones
created by yourself. You will only receive automatic notifications when
somebody else comments or adds changes to the ones that you've created or
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
index 1498f899cf5..cbe5738e7e4 100644
--- a/features/steps/project/issues/award_emoji.rb
+++ b/features/steps/project/issues/award_emoji.rb
@@ -48,7 +48,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
page.within '.awards' do
expect(page).to have_selector '.js-emoji-btn'
expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1'
- expect(page).to have_css(".js-emoji-btn.active[data-original-title='me']")
+ expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
end
end
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index daee90b3767..e21f76d00d9 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -45,6 +45,8 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
step 'I click link "All"' do
click_link "All"
+ # Waits for load
+ expect(find('.issues-state-filters > .active')).to have_content 'All'
end
step 'I click link "Release 0.4"' do
@@ -297,7 +299,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I fill in issue search with \'Rock and roll\'' do
- filter_issue 'Description for issue'
+ filter_issue 'Rock and roll'
end
step 'I should see \'Bugfix1\' in issues' do
@@ -354,8 +356,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
def filter_issue(text)
- sleep 1
fill_in 'issue_search', with: text
- sleep 1
end
end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 53d1aedf27f..9778ff4a6c7 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -22,6 +22,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I click link "All"' do
click_link "All"
+ # Waits for load
+ expect(find('.issues-state-filters > .active')).to have_content 'All'
end
step 'I click link "Merged"' do
@@ -489,7 +491,6 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I fill in merge request search with "Fe"' do
- sleep 1
fill_in 'issue_search', with: "Fe"
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index d43af3f24e9..6b8bfbbdae6 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -43,6 +43,7 @@ module API
mount ::API::CommitStatuses
mount ::API::Commits
mount ::API::DeployKeys
+ mount ::API::Deployments
mount ::API::Environments
mount ::API::Files
mount ::API::Groups
@@ -56,6 +57,7 @@ module API
mount ::API::Milestones
mount ::API::Namespaces
mount ::API::Notes
+ mount ::API::Pipelines
mount ::API::ProjectHooks
mount ::API::ProjectSnippets
mount ::API::Projects
diff --git a/lib/api/builds.rb b/lib/api/builds.rb
index be5a3484ec8..52bdbcae5a8 100644
--- a/lib/api/builds.rb
+++ b/lib/api/builds.rb
@@ -189,6 +189,27 @@ module API
present build, with: Entities::Build,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
+
+ desc 'Trigger a manual build' do
+ success Entities::Build
+ detail 'This feature was added in GitLab 8.11'
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a Build'
+ end
+ post ":id/builds/:build_id/play" do
+ authorize_read_builds!
+
+ build = get_build!(params[:build_id])
+
+ bad_request!("Unplayable Build") unless build.playable?
+
+ build.play(current_user)
+
+ status 200
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
end
helpers do
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
new file mode 100644
index 00000000000..f782bcaf7e9
--- /dev/null
+++ b/lib/api/deployments.rb
@@ -0,0 +1,40 @@
+module API
+ # Deployments RESTfull API endpoints
+ class Deployments < Grape::API
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
+ resource :projects do
+ desc 'Get all deployments of the project' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Deployment
+ end
+ params do
+ optional :page, type: Integer, desc: 'Page number of the current request'
+ optional :per_page, type: Integer, desc: 'Number of items per page'
+ end
+ get ':id/deployments' do
+ authorize! :read_deployment, user_project
+
+ present paginate(user_project.deployments), with: Entities::Deployment
+ end
+
+ desc 'Gets a specific deployment' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Deployment
+ end
+ params do
+ requires :deployment_id, type: Integer, desc: 'The deployment ID'
+ end
+ get ':id/deployments/:deployment_id' do
+ authorize! :read_deployment, user_project
+
+ deployment = user_project.deployments.find(params[:deployment_id])
+
+ present deployment, with: Entities::Deployment
+ end
+ end
+ end
+end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 055716ab1e3..67420772335 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -502,8 +502,28 @@ module API
expose :key, :value
end
+ class Pipeline < Grape::Entity
+ expose :id, :status, :ref, :sha, :before_sha, :tag, :yaml_errors
+
+ expose :user, with: Entities::UserBasic
+ expose :created_at, :updated_at, :started_at, :finished_at, :committed_at
+ expose :duration
+ end
+
class Environment < Grape::Entity
expose :id, :name, :external_url
+ expose :project, using: Entities::Project
+ end
+
+ class EnvironmentBasic < Grape::Entity
+ expose :id, :name, :external_url
+ end
+
+ class Deployment < Grape::Entity
+ expose :id, :iid, :ref, :sha, :created_at
+ expose :user, using: Entities::UserBasic
+ expose :environment, using: Entities::EnvironmentBasic
+ expose :deployable, using: Entities::Build
end
class RepoLicense < Grape::Entity
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
new file mode 100644
index 00000000000..2aae75c471d
--- /dev/null
+++ b/lib/api/pipelines.rb
@@ -0,0 +1,74 @@
+module API
+ class Pipelines < Grape::API
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
+ resource :projects do
+ desc 'Get all Pipelines of the project' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Pipeline
+ end
+ params do
+ optional :page, type: Integer, desc: 'Page number of the current request'
+ optional :per_page, type: Integer, desc: 'Number of items per page'
+ end
+ get ':id/pipelines' do
+ authorize! :read_pipeline, user_project
+
+ present paginate(user_project.pipelines), with: Entities::Pipeline
+ end
+
+ desc 'Gets a specific pipeline for the project' do
+ detail 'This feature was introduced in GitLab 8.11'
+ success Entities::Pipeline
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ get ':id/pipelines/:pipeline_id' do
+ authorize! :read_pipeline, user_project
+
+ present pipeline, with: Entities::Pipeline
+ end
+
+ desc 'Retry failed builds in the pipeline' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Pipeline
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ post ':id/pipelines/:pipeline_id/retry' do
+ authorize! :update_pipeline, user_project
+
+ pipeline.retry_failed(current_user)
+
+ present pipeline, with: Entities::Pipeline
+ end
+
+ desc 'Cancel all builds in the pipeline' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Pipeline
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ post ':id/pipelines/:pipeline_id/cancel' do
+ authorize! :update_pipeline, user_project
+
+ pipeline.cancel_running
+
+ status 200
+ present pipeline.reload, with: Entities::Pipeline
+ end
+ end
+
+ helpers do
+ def pipeline
+ @pipeline ||= user_project.pipelines.find(params[:pipeline_id])
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 260ac81f5fa..9f3b582a263 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -20,8 +20,13 @@ module Ci
build = Ci::RegisterBuildService.new.execute(current_runner)
if build
+ Gitlab::Metrics.add_event(:build_found,
+ project: build.project.path_with_namespace)
+
present build, with: Entities::BuildDetails
else
+ Gitlab::Metrics.add_event(:build_not_found)
+
not_found!
end
end
@@ -42,6 +47,9 @@ module Ci
build.update_attributes(trace: params[:trace]) if params[:trace]
+ Gitlab::Metrics.add_event(:update_build,
+ project: build.project.path_with_namespace)
+
case params[:state].to_s
when 'success'
build.success
diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb
new file mode 100644
index 00000000000..0a1fd27ced5
--- /dev/null
+++ b/lib/gitlab/conflict/file.rb
@@ -0,0 +1,186 @@
+module Gitlab
+ module Conflict
+ class File
+ include Gitlab::Routing.url_helpers
+ include IconsHelper
+
+ class MissingResolution < StandardError
+ end
+
+ CONTEXT_LINES = 3
+
+ attr_reader :merge_file_result, :their_path, :our_path, :our_mode, :merge_request, :repository
+
+ def initialize(merge_file_result, conflict, merge_request:)
+ @merge_file_result = merge_file_result
+ @their_path = conflict[:theirs][:path]
+ @our_path = conflict[:ours][:path]
+ @our_mode = conflict[:ours][:mode]
+ @merge_request = merge_request
+ @repository = merge_request.project.repository
+ @match_line_headers = {}
+ end
+
+ # Array of Gitlab::Diff::Line objects
+ def lines
+ @lines ||= Gitlab::Conflict::Parser.new.parse(merge_file_result[:data],
+ our_path: our_path,
+ their_path: their_path,
+ parent_file: self)
+ end
+
+ def resolve_lines(resolution)
+ section_id = nil
+
+ lines.map do |line|
+ unless line.type
+ section_id = nil
+ next line
+ end
+
+ section_id ||= line_code(line)
+
+ case resolution[section_id]
+ when 'head'
+ next unless line.type == 'new'
+ when 'origin'
+ next unless line.type == 'old'
+ else
+ raise MissingResolution, "Missing resolution for section ID: #{section_id}"
+ end
+
+ line
+ end.compact
+ end
+
+ def highlight_lines!
+ their_file = lines.reject { |line| line.type == 'new' }.map(&:text).join("\n")
+ our_file = lines.reject { |line| line.type == 'old' }.map(&:text).join("\n")
+
+ their_highlight = Gitlab::Highlight.highlight(their_path, their_file, repository: repository).lines
+ our_highlight = Gitlab::Highlight.highlight(our_path, our_file, repository: repository).lines
+
+ lines.each do |line|
+ if line.type == 'old'
+ line.rich_text = their_highlight[line.old_line - 1].try(:html_safe)
+ else
+ line.rich_text = our_highlight[line.new_line - 1].try(:html_safe)
+ end
+ end
+ end
+
+ def sections
+ return @sections if @sections
+
+ chunked_lines = lines.chunk { |line| line.type.nil? }.to_a
+ match_line = nil
+
+ sections_count = chunked_lines.size
+
+ @sections = chunked_lines.flat_map.with_index do |(no_conflict, lines), i|
+ section = nil
+
+ # We need to reduce context sections to CONTEXT_LINES. Conflict sections are
+ # always shown in full.
+ if no_conflict
+ conflict_before = i > 0
+ conflict_after = (sections_count - i) > 1
+
+ if conflict_before && conflict_after
+ # Create a gap in a long context section.
+ if lines.length > CONTEXT_LINES * 2
+ head_lines = lines.first(CONTEXT_LINES)
+ tail_lines = lines.last(CONTEXT_LINES)
+
+ # Ensure any existing match line has text for all lines up to the last
+ # line of its context.
+ update_match_line_text(match_line, head_lines.last)
+
+ # Insert a new match line after the created gap.
+ match_line = create_match_line(tail_lines.first)
+
+ section = [
+ { conflict: false, lines: head_lines },
+ { conflict: false, lines: tail_lines.unshift(match_line) }
+ ]
+ end
+ elsif conflict_after
+ tail_lines = lines.last(CONTEXT_LINES)
+
+ # Create a gap and insert a match line at the start.
+ if lines.length > tail_lines.length
+ match_line = create_match_line(tail_lines.first)
+
+ tail_lines.unshift(match_line)
+ end
+
+ lines = tail_lines
+ elsif conflict_before
+ # We're at the end of the file (no conflicts after), so just remove extra
+ # trailing lines.
+ lines = lines.first(CONTEXT_LINES)
+ end
+ end
+
+ # We want to update the match line's text every time unless we've already
+ # created a gap and its corresponding match line.
+ update_match_line_text(match_line, lines.last) unless section
+
+ section ||= { conflict: !no_conflict, lines: lines }
+ section[:id] = line_code(lines.first) unless no_conflict
+ section
+ end
+ end
+
+ def line_code(line)
+ Gitlab::Diff::LineCode.generate(our_path, line.new_pos, line.old_pos)
+ end
+
+ def create_match_line(line)
+ Gitlab::Diff::Line.new('', 'match', line.index, line.old_pos, line.new_pos)
+ end
+
+ # Any line beginning with a letter, an underscore, or a dollar can be used in a
+ # match line header. Only context sections can contain match lines, as match lines
+ # have to exist in both versions of the file.
+ def find_match_line_header(index)
+ return @match_line_headers[index] if @match_line_headers.key?(index)
+
+ @match_line_headers[index] = begin
+ if index >= 0
+ line = lines[index]
+
+ if line.type.nil? && line.text.match(/\A[A-Za-z$_]/)
+ " #{line.text}"
+ else
+ find_match_line_header(index - 1)
+ end
+ end
+ end
+ end
+
+ # Set the match line's text for the current line. A match line takes its start
+ # position and context header (where present) from itself, and its end position from
+ # the line passed in.
+ def update_match_line_text(match_line, line)
+ return unless match_line
+
+ header = find_match_line_header(match_line.index - 1)
+
+ match_line.text = "@@ -#{match_line.old_pos},#{line.old_pos} +#{match_line.new_pos},#{line.new_pos} @@#{header}"
+ end
+
+ def as_json(opts = nil)
+ {
+ old_path: their_path,
+ new_path: our_path,
+ blob_icon: file_type_icon_class('file', our_mode, our_path),
+ blob_path: namespace_project_blob_path(merge_request.project.namespace,
+ merge_request.project,
+ ::File.join(merge_request.diff_refs.head_sha, our_path)),
+ sections: sections
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb
new file mode 100644
index 00000000000..bbd0427a2c8
--- /dev/null
+++ b/lib/gitlab/conflict/file_collection.rb
@@ -0,0 +1,57 @@
+module Gitlab
+ module Conflict
+ class FileCollection
+ class ConflictSideMissing < StandardError
+ end
+
+ attr_reader :merge_request, :our_commit, :their_commit
+
+ def initialize(merge_request)
+ @merge_request = merge_request
+ @our_commit = merge_request.source_branch_head.raw.raw_commit
+ @their_commit = merge_request.target_branch_head.raw.raw_commit
+ end
+
+ def repository
+ merge_request.project.repository
+ end
+
+ def merge_index
+ @merge_index ||= repository.rugged.merge_commits(our_commit, their_commit)
+ end
+
+ def files
+ @files ||= merge_index.conflicts.map do |conflict|
+ raise ConflictSideMissing unless conflict[:theirs] && conflict[:ours]
+
+ Gitlab::Conflict::File.new(merge_index.merge_file(conflict[:ours][:path]),
+ conflict,
+ merge_request: merge_request)
+ end
+ end
+
+ def as_json(opts = nil)
+ {
+ target_branch: merge_request.target_branch,
+ source_branch: merge_request.source_branch,
+ commit_sha: merge_request.diff_head_sha,
+ commit_message: default_commit_message,
+ files: files
+ }
+ end
+
+ def default_commit_message
+ conflict_filenames = merge_index.conflicts.map do |conflict|
+ "# #{conflict[:ours][:path]}"
+ end
+
+ <<EOM.chomp
+Merge branch '#{merge_request.target_branch}' into '#{merge_request.source_branch}'
+
+# Conflicts:
+#{conflict_filenames.join("\n")}
+EOM
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb
new file mode 100644
index 00000000000..6eccded7872
--- /dev/null
+++ b/lib/gitlab/conflict/parser.rb
@@ -0,0 +1,62 @@
+module Gitlab
+ module Conflict
+ class Parser
+ class ParserError < StandardError
+ end
+
+ class UnexpectedDelimiter < ParserError
+ end
+
+ class MissingEndDelimiter < ParserError
+ end
+
+ class UnmergeableFile < ParserError
+ end
+
+ def parse(text, our_path:, their_path:, parent_file: nil)
+ raise UnmergeableFile if text.blank? # Typically a binary file
+ raise UnmergeableFile if text.length > 102400
+
+ line_obj_index = 0
+ line_old = 1
+ line_new = 1
+ type = nil
+ lines = []
+ conflict_start = "<<<<<<< #{our_path}"
+ conflict_middle = '======='
+ conflict_end = ">>>>>>> #{their_path}"
+
+ text.each_line.map do |line|
+ full_line = line.delete("\n")
+
+ if full_line == conflict_start
+ raise UnexpectedDelimiter unless type.nil?
+
+ type = 'new'
+ elsif full_line == conflict_middle
+ raise UnexpectedDelimiter unless type == 'new'
+
+ type = 'old'
+ elsif full_line == conflict_end
+ raise UnexpectedDelimiter unless type == 'old'
+
+ type = nil
+ elsif line[0] == '\\'
+ type = 'nonewline'
+ lines << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: parent_file)
+ else
+ lines << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: parent_file)
+ line_old += 1 if type != 'new'
+ line_new += 1 if type != 'old'
+
+ line_obj_index += 1
+ end
+ end
+
+ raise MissingEndDelimiter unless type.nil?
+
+ lines
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index cf097e0d0de..80a146b4a5a 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -2,11 +2,13 @@ module Gitlab
module Diff
class Line
attr_reader :type, :index, :old_pos, :new_pos
+ attr_writer :rich_text
attr_accessor :text
- def initialize(text, type, index, old_pos, new_pos)
+ def initialize(text, type, index, old_pos, new_pos, parent_file: nil)
@text, @type, @index = text, type, index
@old_pos, @new_pos = old_pos, new_pos
+ @parent_file = parent_file
end
def self.init_from_hash(hash)
@@ -43,9 +45,25 @@ module Gitlab
type == 'old'
end
+ def rich_text
+ @parent_file.highlight_lines! if @parent_file && !@rich_text
+
+ @rich_text
+ end
+
def meta?
type == 'match' || type == 'nonewline'
end
+
+ def as_json(opts = nil)
+ {
+ type: type,
+ old_line: old_line,
+ new_line: new_line,
+ text: text,
+ rich_text: rich_text || text
+ }
+ end
end
end
end
diff --git a/lib/gitlab/downtime_check/message.rb b/lib/gitlab/downtime_check/message.rb
index 4446e921e0d..40a4815a9a0 100644
--- a/lib/gitlab/downtime_check/message.rb
+++ b/lib/gitlab/downtime_check/message.rb
@@ -1,10 +1,10 @@
module Gitlab
class DowntimeCheck
class Message
- attr_reader :path, :offline, :reason
+ attr_reader :path, :offline
- OFFLINE = "\e[32moffline\e[0m"
- ONLINE = "\e[31monline\e[0m"
+ OFFLINE = "\e[31moffline\e[0m"
+ ONLINE = "\e[32monline\e[0m"
# path - The file path of the migration.
# offline - When set to `true` the migration will require downtime.
@@ -19,10 +19,21 @@ module Gitlab
label = offline ? OFFLINE : ONLINE
message = "[#{label}]: #{path}"
- message += ": #{reason}" if reason
+
+ if reason?
+ message += ":\n\n#{reason}\n\n"
+ end
message
end
+
+ def reason?
+ @reason.present?
+ end
+
+ def reason
+ @reason.strip.lines.map(&:strip).join("\n")
+ end
end
end
end
diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb
index 41fcd971c22..3d1ba33ec68 100644
--- a/lib/gitlab/metrics.rb
+++ b/lib/gitlab/metrics.rb
@@ -124,6 +124,15 @@ module Gitlab
trans.action = action if trans
end
+ # Tracks an event.
+ #
+ # See `Gitlab::Metrics::Transaction#add_event` for more details.
+ def self.add_event(*args)
+ trans = current_transaction
+
+ trans.add_event(*args) if trans
+ end
+
# Returns the prefix to use for the name of a series.
def self.series_prefix
@series_prefix ||= Sidekiq.server? ? 'sidekiq_' : 'rails_'
diff --git a/lib/gitlab/metrics/metric.rb b/lib/gitlab/metrics/metric.rb
index f23d67e1e38..bd0afe53c51 100644
--- a/lib/gitlab/metrics/metric.rb
+++ b/lib/gitlab/metrics/metric.rb
@@ -4,15 +4,20 @@ module Gitlab
class Metric
JITTER_RANGE = 0.000001..0.001
- attr_reader :series, :values, :tags
+ attr_reader :series, :values, :tags, :type
# series - The name of the series (as a String) to store the metric in.
# values - A Hash containing the values to store.
# tags - A Hash containing extra tags to add to the metrics.
- def initialize(series, values, tags = {})
+ def initialize(series, values, tags = {}, type = :metric)
@values = values
@series = series
@tags = tags
+ @type = type
+ end
+
+ def event?
+ type == :event
end
# Returns a Hash in a format that can be directly written to InfluxDB.
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index e61670f491c..b4493bf44d2 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -17,6 +17,10 @@ module Gitlab
begin
retval = trans.run { @app.call(env) }
+ rescue Exception => error # rubocop: disable Lint/RescueException
+ trans.add_event(:rails_exception)
+
+ raise error
# Even in the event of an error we want to submit any metrics we
# might've gathered up to this point.
ensure
diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb
index a1240fd33ee..f9dd8e41912 100644
--- a/lib/gitlab/metrics/sidekiq_middleware.rb
+++ b/lib/gitlab/metrics/sidekiq_middleware.rb
@@ -11,6 +11,10 @@ module Gitlab
# Old gitlad-shell messages don't provide enqueued_at/created_at attributes
trans.set(:sidekiq_queue_duration, Time.now.to_f - (message['enqueued_at'] || message['created_at'] || 0))
trans.run { yield }
+ rescue Exception => error # rubocop: disable Lint/RescueException
+ trans.add_event(:sidekiq_exception)
+
+ raise error
ensure
trans.finish
end
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index 968f3218950..7bc16181be6 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -4,7 +4,10 @@ module Gitlab
class Transaction
THREAD_KEY = :_gitlab_metrics_transaction
- attr_reader :tags, :values, :methods
+ # The series to store events (e.g. Git pushes) in.
+ EVENT_SERIES = 'events'
+
+ attr_reader :tags, :values, :method, :metrics
attr_accessor :action
@@ -55,6 +58,20 @@ module Gitlab
@metrics << Metric.new("#{Metrics.series_prefix}#{series}", values, tags)
end
+ # Tracks a business level event
+ #
+ # Business level events including events such as Git pushes, Emails being
+ # sent, etc.
+ #
+ # event_name - The name of the event (e.g. "git_push").
+ # tags - A set of tags to attach to the event.
+ def add_event(event_name, tags = {})
+ @metrics << Metric.new(EVENT_SERIES,
+ { count: 1 },
+ { event: event_name }.merge(tags),
+ :event)
+ end
+
# Returns a MethodCall object for the given name.
def method_call_for(name)
unless method = @methods[name]
@@ -101,7 +118,7 @@ module Gitlab
submit_hashes = submit.map do |metric|
hash = metric.to_hash
- hash[:tags][:action] ||= @action if @action
+ hash[:tags][:action] ||= @action if @action && !metric.event?
hash
end
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
new file mode 100644
index 00000000000..d0ad5e26dbd
--- /dev/null
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+
+describe Projects::Boards::IssuesController do
+ let(:project) { create(:project_with_board) }
+ let(:user) { create(:user) }
+
+ let(:planning) { create(:label, project: project, name: 'Planning') }
+ let(:development) { create(:label, project: project, name: 'Development') }
+
+ let!(:list1) { create(:list, board: project.board, label: planning, position: 0) }
+ let!(:list2) { create(:list, board: project.board, label: development, position: 1) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe 'GET index' do
+ context 'with valid list id' do
+ it 'returns issues that have the list label applied' do
+ johndoe = create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png')))
+ create(:labeled_issue, project: project, labels: [planning])
+ create(:labeled_issue, project: project, labels: [development])
+ create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
+
+ list_issues user: user, list_id: list2
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(response).to match_response_schema('issues')
+ expect(parsed_response.length).to eq 2
+ end
+ end
+
+ context 'with invalid list id' do
+ it 'returns a not found 404 response' do
+ list_issues user: user, list_id: 999
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with unauthorized user' do
+ before do
+ allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability.abilities).to receive(:allowed?).with(user, :read_issue, project).and_return(false)
+ end
+
+ it 'returns a successful 403 response' do
+ list_issues user: user, list_id: list2
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def list_issues(user:, list_id:)
+ sign_in(user)
+
+ get :index, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ list_id: list_id.to_param
+ end
+ end
+
+ describe 'PATCH update' do
+ let(:issue) { create(:labeled_issue, project: project, labels: [planning]) }
+
+ context 'with valid params' do
+ it 'returns a successful 200 response' do
+ move user: user, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'moves issue to the desired list' do
+ move user: user, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+ expect(issue.reload.labels).to contain_exactly(development)
+ end
+ end
+
+ context 'with invalid params' do
+ it 'returns a unprocessable entity 422 response for invalid lists' do
+ move user: user, issue: issue, from_list_id: nil, to_list_id: nil
+
+ expect(response).to have_http_status(422)
+ end
+
+ it 'returns a not found 404 response for invalid issue id' do
+ move user: user, issue: 999, from_list_id: list1.id, to_list_id: list2.id
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with unauthorized user' do
+ let(:guest) { create(:user) }
+
+ before do
+ project.team << [guest, :guest]
+ end
+
+ it 'returns a successful 403 response' do
+ move user: guest, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def move(user:, issue:, from_list_id:, to_list_id:)
+ sign_in(user)
+
+ patch :update, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: issue.to_param,
+ from_list_id: from_list_id,
+ to_list_id: to_list_id,
+ format: :json
+ end
+ end
+end
diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb
new file mode 100644
index 00000000000..9496636e3cc
--- /dev/null
+++ b/spec/controllers/projects/boards/lists_controller_spec.rb
@@ -0,0 +1,241 @@
+require 'spec_helper'
+
+describe Projects::Boards::ListsController do
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+ let(:user) { create(:user) }
+ let(:guest) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ project.team << [guest, :guest]
+ end
+
+ describe 'GET index' do
+ it 'returns a successful 200 response' do
+ read_board_list user: user
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type).to eq 'application/json'
+ end
+
+ it 'returns a list of board lists' do
+ board = project.create_board
+ create(:backlog_list, board: board)
+ create(:list, board: board)
+ create(:done_list, board: board)
+
+ read_board_list user: user
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(response).to match_response_schema('lists')
+ expect(parsed_response.length).to eq 3
+ end
+
+ context 'with unauthorized user' do
+ before do
+ allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability.abilities).to receive(:allowed?).with(user, :read_list, project).and_return(false)
+ end
+
+ it 'returns a successful 403 response' do
+ read_board_list user: user
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def read_board_list(user:)
+ sign_in(user)
+
+ get :index, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ format: :json
+ end
+ end
+
+ describe 'POST create' do
+ let(:label) { create(:label, project: project, name: 'Development') }
+
+ context 'with valid params' do
+ it 'returns a successful 200 response' do
+ create_board_list user: user, label_id: label.id
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the created list' do
+ create_board_list user: user, label_id: label.id
+
+ expect(response).to match_response_schema('list')
+ end
+ end
+
+ context 'with invalid params' do
+ it 'returns an error' do
+ create_board_list user: user, label_id: nil
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(parsed_response['label']).to contain_exactly "can't be blank"
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'with unauthorized user' do
+ let(:label) { create(:label, project: project, name: 'Development') }
+
+ it 'returns a successful 403 response' do
+ create_board_list user: guest, label_id: label.id
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def create_board_list(user:, label_id:)
+ sign_in(user)
+
+ post :create, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ list: { label_id: label_id },
+ format: :json
+ end
+ end
+
+ describe 'PATCH update' do
+ let!(:planning) { create(:list, board: board, position: 0) }
+ let!(:development) { create(:list, board: board, position: 1) }
+
+ context 'with valid position' do
+ it 'returns a successful 200 response' do
+ move user: user, list: planning, position: 1
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'moves the list to the desired position' do
+ move user: user, list: planning, position: 1
+
+ expect(planning.reload.position).to eq 1
+ end
+ end
+
+ context 'with invalid position' do
+ it 'returns a unprocessable entity 422 response' do
+ move user: user, list: planning, position: 6
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'with invalid list id' do
+ it 'returns a not found 404 response' do
+ move user: user, list: 999, position: 1
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with unauthorized user' do
+ it 'returns a successful 403 response' do
+ move user: guest, list: planning, position: 6
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def move(user:, list:, position:)
+ sign_in(user)
+
+ patch :update, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: list.to_param,
+ list: { position: position },
+ format: :json
+ end
+ end
+
+ describe 'DELETE destroy' do
+ let!(:planning) { create(:list, board: board, position: 0) }
+
+ context 'with valid list id' do
+ it 'returns a successful 200 response' do
+ remove_board_list user: user, list: planning
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'removes list from board' do
+ expect { remove_board_list user: user, list: planning }.to change(board.lists, :size).by(-1)
+ end
+ end
+
+ context 'with invalid list id' do
+ it 'returns a not found 404 response' do
+ remove_board_list user: user, list: 999
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with unauthorized user' do
+ it 'returns a successful 403 response' do
+ remove_board_list user: guest, list: planning
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def remove_board_list(user:, list:)
+ sign_in(user)
+
+ delete :destroy, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: list.to_param,
+ format: :json
+ end
+ end
+
+ describe 'POST generate' do
+ context 'when board lists is empty' do
+ it 'returns a successful 200 response' do
+ generate_default_board_lists user: user
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the defaults lists' do
+ generate_default_board_lists user: user
+
+ expect(response).to match_response_schema('lists')
+ end
+ end
+
+ context 'when board lists is not empty' do
+ it 'returns a unprocessable entity 422 response' do
+ create(:list, board: board)
+
+ generate_default_board_lists user: user
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'with unauthorized user' do
+ it 'returns a successful 403 response' do
+ generate_default_board_lists user: guest
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def generate_default_board_lists(user:)
+ sign_in(user)
+
+ post :generate, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ format: :json
+ end
+ end
+end
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
new file mode 100644
index 00000000000..75a6d39e82c
--- /dev/null
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Projects::BoardsController do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ describe 'GET show' do
+ it 'creates a new board when project does not have one' do
+ expect { read_board }.to change(Board, :count).by(1)
+ end
+
+ it 'renders HTML template' do
+ read_board
+
+ expect(response).to render_template :show
+ expect(response.content_type).to eq 'text/html'
+ end
+
+ context 'with unauthorized user' do
+ before do
+ allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability.abilities).to receive(:allowed?).with(user, :read_board, project).and_return(false)
+ end
+
+ it 'returns a successful 404 response' do
+ read_board
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ def read_board(format: :html)
+ get :show, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ format: format
+ end
+ end
+end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 69758494543..c64c2b075c5 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -4,6 +4,11 @@ describe Projects::MergeRequestsController do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
+ let(:merge_request_with_conflicts) do
+ create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) do |mr|
+ mr.mark_as_unmergeable
+ end
+ end
before do
sign_in(user)
@@ -523,4 +528,135 @@ describe Projects::MergeRequestsController do
end
end
end
+
+ describe 'GET conflicts' do
+ let(:json_response) { JSON.parse(response.body) }
+
+ context 'when the conflicts cannot be resolved in the UI' do
+ before do
+ allow_any_instance_of(Gitlab::Conflict::Parser).
+ to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ get :conflicts,
+ namespace_id: merge_request_with_conflicts.project.namespace.to_param,
+ project_id: merge_request_with_conflicts.project.to_param,
+ id: merge_request_with_conflicts.iid,
+ format: 'json'
+ end
+
+ it 'returns a 200 status code' do
+ expect(response).to have_http_status(:ok)
+ end
+
+ it 'returns JSON with a message' do
+ expect(json_response.keys).to contain_exactly('message', 'type')
+ end
+ end
+
+ context 'with valid conflicts' do
+ before do
+ get :conflicts,
+ namespace_id: merge_request_with_conflicts.project.namespace.to_param,
+ project_id: merge_request_with_conflicts.project.to_param,
+ id: merge_request_with_conflicts.iid,
+ format: 'json'
+ end
+
+ it 'includes meta info about the MR' do
+ expect(json_response['commit_message']).to include('Merge branch')
+ expect(json_response['commit_sha']).to match(/\h{40}/)
+ expect(json_response['source_branch']).to eq(merge_request_with_conflicts.source_branch)
+ expect(json_response['target_branch']).to eq(merge_request_with_conflicts.target_branch)
+ end
+
+ it 'includes each file that has conflicts' do
+ filenames = json_response['files'].map { |file| file['new_path'] }
+
+ expect(filenames).to contain_exactly('files/ruby/popen.rb', 'files/ruby/regex.rb')
+ end
+
+ it 'splits files into sections with lines' do
+ json_response['files'].each do |file|
+ file['sections'].each do |section|
+ expect(section).to include('conflict', 'lines')
+
+ section['lines'].each do |line|
+ if section['conflict']
+ expect(line['type']).to be_in(['old', 'new'])
+ expect(line.values_at('old_line', 'new_line')).to contain_exactly(nil, a_kind_of(Integer))
+ else
+ if line['type'].nil?
+ expect(line['old_line']).not_to eq(nil)
+ expect(line['new_line']).not_to eq(nil)
+ else
+ expect(line['type']).to eq('match')
+ expect(line['old_line']).to eq(nil)
+ expect(line['new_line']).to eq(nil)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ it 'has unique section IDs across files' do
+ section_ids = json_response['files'].flat_map do |file|
+ file['sections'].map { |section| section['id'] }.compact
+ end
+
+ expect(section_ids.uniq).to eq(section_ids)
+ end
+ end
+ end
+
+ context 'POST resolve_conflicts' do
+ let(:json_response) { JSON.parse(response.body) }
+ let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha }
+
+ def resolve_conflicts(sections)
+ post :resolve_conflicts,
+ namespace_id: merge_request_with_conflicts.project.namespace.to_param,
+ project_id: merge_request_with_conflicts.project.to_param,
+ id: merge_request_with_conflicts.iid,
+ format: 'json',
+ sections: sections,
+ commit_message: 'Commit message'
+ end
+
+ context 'with valid params' do
+ before do
+ resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin')
+ end
+
+ it 'creates a new commit on the branch' do
+ expect(original_head_sha).not_to eq(merge_request_with_conflicts.source_branch_head.sha)
+ expect(merge_request_with_conflicts.source_branch_head.message).to include('Commit message')
+ end
+
+ it 'returns an OK response' do
+ expect(response).to have_http_status(:ok)
+ end
+ end
+
+ context 'when sections are missing' do
+ before do
+ resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head')
+ end
+
+ it 'returns a 400 error' do
+ expect(response).to have_http_status(:bad_request)
+ end
+
+ it 'has a message with the name of the first missing section' do
+ expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9')
+ end
+
+ it 'does not create a new commit' do
+ expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
+ end
+ end
+ end
end
diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb
new file mode 100644
index 00000000000..35c4a0b6f08
--- /dev/null
+++ b/spec/factories/boards.rb
@@ -0,0 +1,5 @@
+FactoryGirl.define do
+ factory :board do
+ project factory: :empty_project
+ end
+end
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index 04d66020c87..ac2a1ba5dff 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -1,21 +1,3 @@
-# == Schema Information
-#
-# Table name: commits
-#
-# id :integer not null, primary key
-# project_id :integer
-# ref :string(255)
-# sha :string(255)
-# before_sha :string(255)
-# push_data :text
-# created_at :datetime
-# updated_at :datetime
-# tag :boolean default(FALSE)
-# yaml_errors :text
-# committed_at :datetime
-# gl_project_id :integer
-#
-
FactoryGirl.define do
factory :ci_empty_pipeline, class: Ci::Pipeline do
ref 'master'
diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb
new file mode 100644
index 00000000000..9e3f06c682c
--- /dev/null
+++ b/spec/factories/lists.rb
@@ -0,0 +1,20 @@
+FactoryGirl.define do
+ factory :list do
+ board
+ label
+ list_type :label
+ sequence(:position)
+ end
+
+ factory :backlog_list, parent: :list do
+ list_type :backlog
+ label nil
+ position nil
+ end
+
+ factory :done_list, parent: :list do
+ list_type :done
+ label nil
+ position nil
+ end
+end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index b682ced75ac..f82d68a1816 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -83,4 +83,10 @@ FactoryGirl.define do
)
end
end
+
+ factory :project_with_board, parent: :empty_project do
+ after(:create) do |project|
+ project.create_board
+ end
+ end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
new file mode 100644
index 00000000000..8910c50c294
--- /dev/null
+++ b/spec/features/boards/boards_spec.rb
@@ -0,0 +1,622 @@
+require 'rails_helper'
+
+describe 'Issue Boards', feature: true, js: true do
+ include WaitForAjax
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ let!(:user2) { create(:user) }
+
+ before do
+ project.create_board
+ project.board.lists.create(list_type: :backlog)
+ project.board.lists.create(list_type: :done)
+
+ project.team << [user, :master]
+ project.team << [user2, :master]
+
+ login_as(user)
+ end
+
+ context 'no lists' do
+ before do
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+ expect(page).to have_selector('.board', count: 3)
+ end
+
+ it 'shows blank state' do
+ expect(page).to have_content('Welcome to your Issue Board!')
+ end
+
+ it 'hides the blank state when clicking nevermind button' do
+ page.within(find('.board-blank-state')) do
+ click_button("Nevermind, I'll use my own")
+ end
+ expect(page).to have_selector('.board', count: 2)
+ end
+
+ it 'creates default lists' do
+ lists = ['Backlog', 'Development', 'Testing', 'Production', 'Ready', 'Done']
+
+ page.within(find('.board-blank-state')) do
+ click_button('Add default lists')
+ end
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 6)
+
+ page.all('.board').each_with_index do |list, i|
+ expect(list.find('.board-title')).to have_content(lists[i])
+ end
+ end
+ end
+
+ context 'with lists' do
+ let(:milestone) { create(:milestone, project: project) }
+
+ let(:planning) { create(:label, project: project, name: 'Planning') }
+ let(:development) { create(:label, project: project, name: 'Development') }
+ let(:testing) { create(:label, project: project, name: 'Testing') }
+ let(:bug) { create(:label, project: project, name: 'Bug') }
+ let!(:backlog) { create(:label, project: project, name: 'Backlog') }
+ let!(:done) { create(:label, project: project, name: 'Done') }
+
+ let!(:list1) { create(:list, board: project.board, label: planning, position: 0) }
+ let!(:list2) { create(:list, board: project.board, label: development, position: 1) }
+
+ let!(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
+ let!(:issue1) { create(:issue, project: project, assignee: user) }
+ let!(:issue2) { create(:issue, project: project, author: user2) }
+ let!(:issue3) { create(:issue, project: project) }
+ let!(:issue4) { create(:issue, project: project) }
+ let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone) }
+ let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) }
+ let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) }
+ let!(:issue8) { create(:closed_issue, project: project) }
+ let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug]) }
+
+ before do
+ visit namespace_project_board_path(project.namespace, project)
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 4)
+ expect(find('.board:nth-child(1)')).to have_selector('.card')
+ expect(find('.board:nth-child(2)')).to have_selector('.card')
+ expect(find('.board:nth-child(3)')).to have_selector('.card')
+ expect(find('.board:nth-child(4)')).to have_selector('.card')
+ end
+
+ it 'shows lists' do
+ expect(page).to have_selector('.board', count: 4)
+ end
+
+ it 'shows issues in lists' do
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('2')
+ expect(page).to have_selector('.card', count: 2)
+ end
+
+ page.within(find('.board:nth-child(3)')) do
+ expect(page.find('.board-header')).to have_content('2')
+ expect(page).to have_selector('.card', count: 2)
+ end
+ end
+
+ it 'shows confidential issues with icon' do
+ page.within(find('.board', match: :first)) do
+ expect(page).to have_selector('.confidential-icon', count: 1)
+ end
+ end
+
+ it 'allows user to delete board' do
+ page.within(find('.board:nth-child(2)')) do
+ find('.board-delete').click
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 3)
+ end
+
+ it 'removes checkmark in new list dropdown after deleting' do
+ click_button 'Create new list'
+ wait_for_ajax
+
+ page.within(find('.board:nth-child(2)')) do
+ find('.board-delete').click
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 3)
+ expect(find(".js-board-list-#{planning.id}", visible: false)).not_to have_css('.is-active')
+ end
+
+ it 'infinite scrolls list' do
+ 50.times do
+ create(:issue, project: project)
+ end
+
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('20')
+ expect(page).to have_selector('.card', count: 20)
+
+ evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
+ wait_for_vue_resource(spinner: false)
+
+ expect(page.find('.board-header')).to have_content('40')
+ expect(page).to have_selector('.card', count: 40)
+ end
+ end
+
+ context 'backlog' do
+ it 'shows issues in backlog with no labels' do
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('6')
+ expect(page).to have_selector('.card', count: 6)
+ end
+ end
+
+ it 'is searchable' do
+ page.within(find('.board', match: :first)) do
+ find('.form-control').set issue1.title
+
+ wait_for_vue_resource(spinner: false)
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+
+ it 'clears search' do
+ page.within(find('.board', match: :first)) do
+ find('.form-control').set issue1.title
+
+ expect(page).to have_selector('.card', count: 1)
+
+ find('.board-search-clear-btn').click
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page).to have_selector('.card', count: 6)
+ end
+ end
+
+ it 'moves issue from backlog into list' do
+ drag_to(list_to_index: 1)
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('5')
+ expect(page).to have_selector('.card', count: 5)
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('3')
+ expect(page).to have_selector('.card', count: 3)
+ end
+ end
+ end
+
+ context 'done' do
+ it 'shows list of done issues' do
+ expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1)
+ end
+
+ it 'moves issue to done' do
+ drag_to(list_from_index: 0, list_to_index: 3)
+
+ expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2)
+ expect(find('.board:nth-child(4)')).to have_content(issue9.title)
+ expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
+ end
+
+ it 'removes all of the same issue to done' do
+ drag_to(list_from_index: 1, list_to_index: 3)
+
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(4)')).to have_content(issue6.title)
+ expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
+ end
+ end
+
+ context 'lists' do
+ it 'changes position of list' do
+ drag_to(list_from_index: 1, list_to_index: 2, selector: '.board-header')
+
+ expect(find('.board:nth-child(2)')).to have_content(development.title)
+ expect(find('.board:nth-child(2)')).to have_content(planning.title)
+ end
+
+ it 'issue moves between lists' do
+ drag_to(list_from_index: 1, card_index: 1, list_to_index: 2)
+
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 3)
+ expect(find('.board:nth-child(3)')).to have_content(issue6.title)
+ expect(find('.board:nth-child(3)').all('.card').last).not_to have_content(development.title)
+ end
+
+ it 'issue moves between lists' do
+ drag_to(list_from_index: 2, list_to_index: 1)
+
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 3)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(2)')).to have_content(issue7.title)
+ expect(find('.board:nth-child(2)').all('.card').first).not_to have_content(planning.title)
+ end
+
+ it 'issue moves from done' do
+ drag_to(list_from_index: 3, list_to_index: 1)
+
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 3)
+ expect(find('.board:nth-child(2)')).to have_content(issue8.title)
+ end
+
+ context 'issue card' do
+ it 'shows assignee' do
+ page.within(find('.board', match: :first)) do
+ expect(page).to have_selector('.avatar', count: 1)
+ end
+ end
+ end
+
+ context 'new list' do
+ it 'shows all labels in new list dropdown' do
+ click_button 'Create new list'
+ wait_for_ajax
+
+ page.within('.dropdown-menu-issues-board-new') do
+ expect(page).to have_content(planning.title)
+ expect(page).to have_content(development.title)
+ expect(page).to have_content(testing.title)
+ end
+ end
+
+ it 'creates new list for label' do
+ click_button 'Create new list'
+ wait_for_ajax
+
+ page.within('.dropdown-menu-issues-board-new') do
+ click_link testing.title
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 5)
+ end
+
+ it 'creates new list for Backlog label' do
+ click_button 'Create new list'
+ wait_for_ajax
+
+ page.within('.dropdown-menu-issues-board-new') do
+ click_link backlog.title
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 5)
+ end
+
+ it 'creates new list for Done label' do
+ click_button 'Create new list'
+ wait_for_ajax
+
+ page.within('.dropdown-menu-issues-board-new') do
+ click_link done.title
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 5)
+ end
+
+ it 'moves issues from backlog into new list' do
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('6')
+ expect(page).to have_selector('.card', count: 6)
+ end
+
+ click_button 'Create new list'
+ wait_for_ajax
+
+ page.within('.dropdown-menu-issues-board-new') do
+ click_link testing.title
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('5')
+ expect(page).to have_selector('.card', count: 5)
+ end
+ end
+ end
+ end
+
+ context 'filtering' do
+ it 'filters by author' do
+ page.within '.issues-filters' do
+ click_button('Author')
+ wait_for_ajax
+
+ page.within '.dropdown-menu-author' do
+ click_link(user2.name)
+ end
+ wait_for_vue_resource(spinner: false)
+
+ expect(find('.js-author-search')).to have_content(user2.name)
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('1')
+ expect(page).to have_selector('.card', count: 1)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+ end
+
+ it 'filters by assignee' do
+ page.within '.issues-filters' do
+ click_button('Assignee')
+ wait_for_ajax
+
+ page.within '.dropdown-menu-assignee' do
+ click_link(user.name)
+ end
+ wait_for_vue_resource(spinner: false)
+
+ expect(find('.js-assignee-search')).to have_content(user.name)
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('1')
+ expect(page).to have_selector('.card', count: 1)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+ end
+
+ it 'filters by milestone' do
+ page.within '.issues-filters' do
+ click_button('Milestone')
+ wait_for_ajax
+
+ page.within '.milestone-filter' do
+ click_link(milestone.title)
+ end
+ wait_for_vue_resource(spinner: false)
+
+ expect(find('.js-milestone-select')).to have_content(milestone.title)
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('1')
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+
+ it 'filters by label' do
+ page.within '.issues-filters' do
+ click_button('Label')
+ wait_for_ajax
+
+ page.within '.dropdown-menu-labels' do
+ click_link(testing.title)
+ wait_for_vue_resource(spinner: false)
+ find('.dropdown-menu-close').click
+ end
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('1')
+ expect(page).to have_selector('.card', count: 1)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+ end
+
+ it 'infinite scrolls list with label filter' do
+ 50.times do
+ create(:labeled_issue, project: project, labels: [testing])
+ end
+
+ page.within '.issues-filters' do
+ click_button('Label')
+ wait_for_ajax
+
+ page.within '.dropdown-menu-labels' do
+ click_link(testing.title)
+ wait_for_vue_resource(spinner: false)
+ find('.dropdown-menu-close').click
+ end
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('20')
+ expect(page).to have_selector('.card', count: 20)
+
+ evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
+
+ expect(page.find('.board-header')).to have_content('40')
+ expect(page).to have_selector('.card', count: 40)
+ end
+ end
+
+ it 'filters by multiple labels' do
+ page.within '.issues-filters' do
+ click_button('Label')
+ wait_for_ajax
+
+ page.within(find('.dropdown-menu-labels')) do
+ click_link(testing.title)
+ wait_for_vue_resource(spinner: false)
+ click_link(bug.title)
+ wait_for_vue_resource(spinner: false)
+ find('.dropdown-menu-close').click
+ end
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('1')
+ expect(page).to have_selector('.card', count: 1)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+ end
+
+ it 'filters by no label' do
+ page.within '.issues-filters' do
+ click_button('Label')
+ wait_for_ajax
+
+ page.within '.dropdown-menu-labels' do
+ click_link("No Label")
+ wait_for_vue_resource(spinner: false)
+ find('.dropdown-menu-close').click
+ end
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('5')
+ expect(page).to have_selector('.card', count: 5)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+ end
+
+ it 'filters by clicking label button on issue' do
+ page.within(find('.board', match: :first)) do
+ expect(page).to have_selector('.card', count: 6)
+ expect(find('.card', match: :first)).to have_content(bug.title)
+ click_button(bug.title)
+ wait_for_vue_resource(spinner: false)
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('1')
+ expect(page).to have_selector('.card', count: 1)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+
+ page.within('.labels-filter') do
+ expect(find('.dropdown-toggle-text')).to have_content(bug.title)
+ end
+ end
+
+ it 'removes label filter by clicking label button on issue' do
+ page.within(find('.board', match: :first)) do
+ page.within(find('.card', match: :first)) do
+ click_button(bug.title)
+ end
+ wait_for_vue_resource(spinner: false)
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+
+ wait_for_vue_resource
+
+ page.within('.labels-filter') do
+ expect(find('.dropdown-toggle-text')).to have_content(bug.title)
+ end
+ end
+ end
+ end
+
+ context 'signed out user' do
+ before do
+ logout
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+ end
+
+ it 'does not show create new list' do
+ expect(page).not_to have_selector('.js-new-board-list')
+ end
+ end
+
+ context 'as guest user' do
+ let(:user_guest) { create(:user) }
+
+ before do
+ project.team << [user_guest, :guest]
+ logout
+ login_as(user_guest)
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+ end
+
+ it 'does not show create new list' do
+ expect(page).not_to have_selector('.js-new-board-list')
+ end
+ end
+
+ def drag_to(list_from_index: 0, card_index: 0, to_index: 0, list_to_index: 0, selector: '.board-list')
+ evaluate_script("simulateDrag({scrollable: document.getElementById('board-app'), from: {el: $('#{selector}').eq(#{list_from_index}).get(0), index: #{card_index}}, to: {el: $('.board-list').eq(#{list_to_index}).get(0), index: #{to_index}}});")
+
+ Timeout.timeout(Capybara.default_max_wait_time) do
+ loop until page.evaluate_script('window.SIMULATE_DRAG_ACTIVE').zero?
+ end
+
+ wait_for_vue_resource
+ end
+
+ def wait_for_vue_resource(spinner: true)
+ Timeout.timeout(Capybara.default_max_wait_time) do
+ loop until page.evaluate_script('Vue.activeResources').zero?
+ end
+
+ if spinner
+ expect(find('.boards-list')).not_to have_selector('.fa-spinner')
+ end
+ end
+end
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
new file mode 100644
index 00000000000..930c36ade2b
--- /dev/null
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+feature 'Merge request conflict resolution', js: true, feature: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ def create_merge_request(source_branch)
+ create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start', source_project: project) do |mr|
+ mr.mark_as_unmergeable
+ end
+ end
+
+ context 'when a merge request can be resolved in the UI' do
+ let(:merge_request) { create_merge_request('conflict-resolvable') }
+
+ before do
+ project.team << [user, :developer]
+ login_as(user)
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'shows a link to the conflict resolution page' do
+ expect(page).to have_link('conflicts', href: /\/conflicts\Z/)
+ end
+
+ context 'visiting the conflicts resolution page' do
+ before { click_link('conflicts', href: /\/conflicts\Z/) }
+
+ it 'shows the conflicts' do
+ begin
+ expect(find('#conflicts')).to have_content('popen.rb')
+ rescue Capybara::Poltergeist::JavascriptError
+ retry
+ end
+ end
+ end
+ end
+
+ UNRESOLVABLE_CONFLICTS = {
+ 'conflict-too-large' => 'when the conflicts contain a large file',
+ 'conflict-binary-file' => 'when the conflicts contain a binary file',
+ 'conflict-contains-conflict-markers' => 'when the conflicts contain a file with ambiguous conflict markers',
+ 'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another'
+ }
+
+ UNRESOLVABLE_CONFLICTS.each do |source_branch, description|
+ context description do
+ let(:merge_request) { create_merge_request(source_branch) }
+
+ before do
+ project.team << [user, :developer]
+ login_as(user)
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'does not show a link to the conflict resolution page' do
+ expect(page).not_to have_link('conflicts', href: /\/conflicts\Z/)
+ end
+
+ it 'shows an error if the conflicts page is visited directly' do
+ visit current_url + '/conflicts'
+ wait_for_ajax
+
+ expect(find('#conflicts')).to have_content('Please try to resolve them locally.')
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index 11c9de3c4bf..b963d1305b5 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -8,10 +8,11 @@ feature 'Create New Merge Request', feature: true, js: true do
project.team << [user, :master]
login_as user
- visit namespace_project_merge_requests_path(project.namespace, project)
end
it 'generates a diff for an orphaned branch' do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+
click_link 'New Merge Request'
expect(page).to have_content('Source branch')
expect(page).to have_content('Target branch')
@@ -42,4 +43,20 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).not_to have_content private_project.to_reference
end
end
+
+ it 'allows to change the diff view' do
+ visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'master', source_branch: 'fix' })
+
+ click_link 'Changes'
+
+ expect(page).to have_css('a.btn.active', text: 'Inline')
+ expect(page).not_to have_css('a.btn.active', text: 'Side-by-side')
+
+ click_link 'Side-by-side'
+
+ within '.merge-request' do
+ expect(page).not_to have_css('a.btn.active', text: 'Inline')
+ expect(page).to have_css('a.btn.active', text: 'Side-by-side')
+ end
+ end
end
diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_requests/pipelines_spec.rb
new file mode 100644
index 00000000000..9c4c0525267
--- /dev/null
+++ b/spec/features/merge_requests/pipelines_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+feature 'Pipelines for Merge Requests', feature: true, js: true do
+ include WaitForAjax
+
+ given(:user) { create(:user) }
+ given(:merge_request) { create(:merge_request) }
+ given(:project) { merge_request.target_project }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ end
+
+ context 'with pipelines' do
+ let!(:pipeline) do
+ create(:ci_empty_pipeline,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ scenario 'user visits merge request pipelines tab' do
+ page.within('.merge-request-tabs') do
+ click_link('Pipelines')
+ end
+ wait_for_ajax
+
+ expect(page).to have_selector('.pipeline-actions')
+ end
+ end
+
+ context 'without pipelines' do
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ scenario 'user visits merge request page' do
+ page.within('.merge-request-tabs') do
+ expect(page).to have_no_link('Pipelines')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/issues/list_spec.rb b/spec/features/projects/issues/list_spec.rb
new file mode 100644
index 00000000000..3137af074ca
--- /dev/null
+++ b/spec/features/projects/issues/list_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+feature 'Issues List' do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+
+ background do
+ project.team << [user, :developer]
+
+ login_as(user)
+ end
+
+ scenario 'user does not see create new list button' do
+ create(:issue, project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+
+ expect(page).not_to have_selector('.js-new-board-list')
+ end
+end
diff --git a/spec/features/projects/merge_requests/list_spec.rb b/spec/features/projects/merge_requests/list_spec.rb
new file mode 100644
index 00000000000..5dd58ad66a7
--- /dev/null
+++ b/spec/features/projects/merge_requests/list_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+feature 'Merge Requests List' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ background do
+ project.team << [user, :developer]
+
+ login_as(user)
+ end
+
+ scenario 'user does not see create new list button' do
+ create(:merge_request, source_project: project)
+
+ visit namespace_project_merge_requests_path(project.namespace, project)
+
+ expect(page).not_to have_selector('.js-new-board-list')
+ end
+end
diff --git a/spec/features/projects/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb
index 29d150bc597..47482bc3cc9 100644
--- a/spec/features/projects/pipelines_spec.rb
+++ b/spec/features/projects/pipelines_spec.rb
@@ -193,7 +193,11 @@ describe "Pipelines" do
end
context 'playing manual build' do
- before { click_link('Play') }
+ before do
+ within '.pipeline-holder' do
+ click_link('Play')
+ end
+ end
it { expect(@manual.reload).to be_pending }
end
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index d370f90f7d9..a46e48c76ed 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -12,10 +12,12 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
end
def register_u2f_device(u2f_device = nil)
- u2f_device ||= FakeU2fDevice.new(page)
+ name = FFaker::Name.first_name
+ u2f_device ||= FakeU2fDevice.new(page, name)
u2f_device.respond_to_u2f_registration
click_on 'Setup New U2F Device'
expect(page).to have_content('Your device was successfully set up')
+ fill_in "Pick a name", with: name
click_on 'Register U2F Device'
u2f_device
end
@@ -40,13 +42,14 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
end
describe 'when 2FA via OTP is enabled' do
- it 'allows registering a new device' do
+ it 'allows registering a new device with a name' do
visit profile_account_path
manage_two_factor_authentication
expect(page.body).to match("You've already enabled two-factor authentication using mobile")
- register_u2f_device
+ u2f_device = register_u2f_device
+ expect(page.body).to match(u2f_device.name)
expect(page.body).to match('Your U2F device was registered')
end
@@ -55,15 +58,31 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
# First device
manage_two_factor_authentication
- register_u2f_device
+ first_device = register_u2f_device
expect(page.body).to match('Your U2F device was registered')
# Second device
- manage_two_factor_authentication
- register_u2f_device
+ second_device = register_u2f_device
expect(page.body).to match('Your U2F device was registered')
+
+ expect(page.body).to match(first_device.name)
+ expect(page.body).to match(second_device.name)
+ expect(U2fRegistration.count).to eq(2)
+ end
+
+ it 'allows deleting a device' do
+ visit profile_account_path
manage_two_factor_authentication
- expect(page.body).to match('You have 2 U2F devices registered')
+ expect(page.body).to match("You've already enabled two-factor authentication using mobile")
+
+ first_u2f_device = register_u2f_device
+ second_u2f_device = register_u2f_device
+
+ click_on "Delete", match: :first
+
+ expect(page.body).to match('Successfully deleted')
+ expect(page.body).not_to match(first_u2f_device.name)
+ expect(page.body).to match(second_u2f_device.name)
end
end
@@ -208,7 +227,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
describe "when a given U2F device has not been registered" do
it "does not allow logging in with that particular device" do
- unregistered_device = FakeU2fDevice.new(page)
+ unregistered_device = FakeU2fDevice.new(page, FFaker::Name.first_name)
login_as(user)
unregistered_device.respond_to_u2f_authentication
click_on "Login Via U2F Device"
@@ -262,6 +281,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
end
it "deletes u2f registrations" do
+ visit profile_account_path
expect { click_on "Disable" }.to change { U2fRegistration.count }.by(-1)
end
end
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
new file mode 100644
index 00000000000..299e4675d6f
--- /dev/null
+++ b/spec/fixtures/api/schemas/issue.json
@@ -0,0 +1,40 @@
+{
+ "type": "object",
+ "required" : [
+ "iid",
+ "title",
+ "confidential"
+ ],
+ "properties" : {
+ "iid": { "type": "integer" },
+ "title": { "type": "string" },
+ "confidential": { "type": "boolean" },
+ "labels": {
+ "type": ["array"],
+ "required": [
+ "id",
+ "color",
+ "description",
+ "title",
+ "priority"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+ },
+ "description": { "type": ["string", "null"] },
+ "title": { "type": "string" },
+ "priority": { "type": ["integer", "null"] }
+ }
+ },
+ "assignee": {
+ "id": { "type": "integet" },
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "avatar_url": { "type": "uri" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/issues.json b/spec/fixtures/api/schemas/issues.json
new file mode 100644
index 00000000000..0d2067f704a
--- /dev/null
+++ b/spec/fixtures/api/schemas/issues.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "issue.json" }
+}
diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json
new file mode 100644
index 00000000000..f070fa3b254
--- /dev/null
+++ b/spec/fixtures/api/schemas/list.json
@@ -0,0 +1,39 @@
+{
+ "type": "object",
+ "required" : [
+ "id",
+ "list_type",
+ "title",
+ "position"
+ ],
+ "properties" : {
+ "id": { "type": "integer" },
+ "list_type": {
+ "type": "string",
+ "enum": ["backlog", "label", "done"]
+ },
+ "label": {
+ "type": ["object"],
+ "required": [
+ "id",
+ "color",
+ "description",
+ "title",
+ "priority"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+ },
+ "description": { "type": ["string", "null"] },
+ "title": { "type": "string" },
+ "priority": { "type": ["integer", "null"] }
+ }
+ },
+ "title": { "type": "string" },
+ "position": { "type": ["integer", "null"] }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/lists.json b/spec/fixtures/api/schemas/lists.json
new file mode 100644
index 00000000000..9f618aa9de5
--- /dev/null
+++ b/spec/fixtures/api/schemas/lists.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "list.json" }
+}
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 94972eed945..a43a7238c70 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -69,18 +69,40 @@ describe BlobHelper do
end
describe "#edit_blob_link" do
- let(:project) { create(:project) }
+ let(:namespace) { create(:namespace, name: 'gitlab' )}
+ let(:project) { create(:project, namespace: namespace) }
before do
allow(self).to receive(:current_user).and_return(double)
+ allow(self).to receive(:can_collaborate_with_project?).and_return(true)
end
it 'verifies blob is text' do
- expect(self).not_to receive(:blob_text_viewable?)
+ expect(helper).not_to receive(:blob_text_viewable?)
button = edit_blob_link(project, 'refs/heads/master', 'README.md')
expect(button).to start_with('<button')
end
+
+ it 'uses the passed blob instead retrieve from repository' do
+ blob = project.repository.blob_at('refs/heads/master', 'README.md')
+
+ expect(project.repository).not_to receive(:blob_at)
+
+ edit_blob_link(project, 'refs/heads/master', 'README.md', blob: blob)
+ end
+
+ it 'returns a link with the proper route' do
+ link = edit_blob_link(project, 'master', 'README.md')
+
+ expect(Capybara.string(link).find_link('Edit')[:href]).to eq('/gitlab/gitlabhq/edit/master/README.md')
+ end
+
+ it 'returns a link with the passed link_opts on the expected route' do
+ link = edit_blob_link(project, 'master', 'README.md', link_opts: { mr_id: 10 })
+
+ expect(Capybara.string(link).find_link('Edit')[:href]).to eq('/gitlab/gitlabhq/edit/master/README.md?mr_id=10')
+ end
end
end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 5e4655dfc95..67bac782591 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -62,6 +62,32 @@ describe IssuesHelper do
it { is_expected.to eq("!1, !2, or !3") }
end
+ describe '#award_user_list' do
+ let!(:awards) { build_list(:award_emoji, 15) }
+
+ it "returns a comma seperated list of 1-9 users" do
+ expect(award_user_list(awards.first(9), nil)).to eq(awards.first(9).map { |a| a.user.name }.to_sentence)
+ end
+
+ it "displays the current user's name as 'You'" do
+ expect(award_user_list(awards.first(1), awards[0].user)).to eq('You')
+ end
+
+ it "truncates lists of larger than 9 users" do
+ expect(award_user_list(awards, nil)).to eq(awards.first(9).map { |a| a.user.name }.join(', ') + ", and 6 more.")
+ end
+
+ it "displays the current user in front of 0-9 other users" do
+ expect(award_user_list(awards, awards[0].user)).
+ to eq("You, " + awards[1..9].map { |a| a.user.name }.join(', ') + ", and 5 more.")
+ end
+
+ it "displays the current user in front regardless of position in the list" do
+ expect(award_user_list(awards, awards[12].user)).
+ to eq("You, " + awards[0..8].map { |a| a.user.name }.join(', ') + ", and 5 more.")
+ end
+ end
+
describe '#award_active_class' do
let!(:upvote) { create(:award_emoji) }
diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6
new file mode 100644
index 00000000000..6bcfdf191c2
--- /dev/null
+++ b/spec/javascripts/abuse_reports_spec.js.es6
@@ -0,0 +1,41 @@
+/*= require abuse_reports */
+
+/*= require jquery */
+
+((global) => {
+ const FIXTURE = 'abuse_reports.html';
+ const MAX_MESSAGE_LENGTH = 500;
+
+ function assertMaxLength($message) {
+ expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
+ }
+
+ describe('Abuse Reports', function() {
+ fixture.preload(FIXTURE);
+
+ beforeEach(function() {
+ fixture.load(FIXTURE);
+ new global.AbuseReports();
+ });
+
+ it('should truncate long messages', function() {
+ const $longMessage = $('#long');
+ expect($longMessage.data('original-message')).toEqual(jasmine.anything());
+ assertMaxLength($longMessage);
+ });
+
+ it('should not truncate short messages', function() {
+ const $shortMessage = $('#short');
+ expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything());
+ });
+
+ it('should allow clicking a truncated message to expand and collapse the full message', function() {
+ const $longMessage = $('#long');
+ $longMessage.click();
+ expect($longMessage.data('original-message').length).toEqual($longMessage.text().length);
+ $longMessage.click();
+ assertMaxLength($longMessage);
+ });
+ });
+
+})(window.gl);
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index 3ddc163033e..fa32d0d7da5 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -143,6 +143,52 @@
return expect($votesBlock.find('[data-emoji=fire]').length).toBe(0);
});
});
+ describe('::addYouToUserList', function() {
+ it('should prepend "You" to the award tooltip', function() {
+ var $thumbsUpEmoji, $votesBlock, awardUrl;
+ awardUrl = awardsHandler.getAwardUrl();
+ $votesBlock = $('.js-awards-block').eq(0);
+ $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji.attr('data-title', 'sam, jerry, max, and andy');
+ awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
+ $thumbsUpEmoji.tooltip();
+ return expect($thumbsUpEmoji.data("original-title")).toBe('You, sam, jerry, max, and andy');
+ });
+ return it('handles the special case where "You" is not cleanly comma seperated', function() {
+ var $thumbsUpEmoji, $votesBlock, awardUrl;
+ awardUrl = awardsHandler.getAwardUrl();
+ $votesBlock = $('.js-awards-block').eq(0);
+ $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji.attr('data-title', 'sam');
+ awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
+ $thumbsUpEmoji.tooltip();
+ return expect($thumbsUpEmoji.data("original-title")).toBe('You and sam');
+ });
+ });
+ describe('::removeYouToUserList', function() {
+ it('removes "You" from the front of the tooltip', function() {
+ var $thumbsUpEmoji, $votesBlock, awardUrl;
+ awardUrl = awardsHandler.getAwardUrl();
+ $votesBlock = $('.js-awards-block').eq(0);
+ $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji.attr('data-title', 'You, sam, jerry, max, and andy');
+ $thumbsUpEmoji.addClass('active');
+ awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
+ $thumbsUpEmoji.tooltip();
+ return expect($thumbsUpEmoji.data("original-title")).toBe('sam, jerry, max, and andy');
+ });
+ return it('handles the special case where "You" is not cleanly comma seperated', function() {
+ var $thumbsUpEmoji, $votesBlock, awardUrl;
+ awardUrl = awardsHandler.getAwardUrl();
+ $votesBlock = $('.js-awards-block').eq(0);
+ $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji.attr('data-title', 'You and sam');
+ $thumbsUpEmoji.addClass('active');
+ awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
+ $thumbsUpEmoji.tooltip();
+ return expect($thumbsUpEmoji.data("original-title")).toBe('sam');
+ });
+ });
describe('search', function() {
return it('should filter the emoji', function() {
$('.js-add-award').eq(0).click();
diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6
new file mode 100644
index 00000000000..078e4b00023
--- /dev/null
+++ b/spec/javascripts/boards/boards_store_spec.js.es6
@@ -0,0 +1,164 @@
+//= require jquery
+//= require jquery_ujs
+//= require jquery.cookie
+//= require vue
+//= require vue-resource
+//= require lib/utils/url_utility
+//= require boards/models/issue
+//= require boards/models/label
+//= require boards/models/list
+//= require boards/models/user
+//= require boards/services/board_service
+//= require boards/stores/boards_store
+//= require ./mock_data
+
+(() => {
+ beforeEach(() => {
+ gl.boardService = new BoardService('/test/issue-boards/board');
+ gl.issueBoards.BoardsStore.create();
+
+ $.cookie('issue_board_welcome_hidden', 'false');
+ });
+
+ describe('Store', () => {
+ it('starts with a blank state', () => {
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
+ });
+
+ describe('lists', () => {
+ it('creates new list without persisting to DB', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+ });
+
+ it('finds list by ID', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ const list = gl.issueBoards.BoardsStore.findList('id', 1);
+
+ expect(list.id).toBe(1);
+ });
+
+ it('finds list by type', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ const list = gl.issueBoards.BoardsStore.findList('type', 'label');
+
+ expect(list).toBeDefined();
+ });
+
+ it('finds list limited by type', () => {
+ gl.issueBoards.BoardsStore.addList({
+ id: 1,
+ position: 0,
+ title: 'Test',
+ list_type: 'backlog'
+ });
+ const list = gl.issueBoards.BoardsStore.findList('id', 1, 'backlog');
+
+ expect(list).toBeDefined();
+ });
+
+ it('gets issue when new list added', (done) => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ const list = gl.issueBoards.BoardsStore.findList('id', 1);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+
+ setTimeout(() => {
+ expect(list.issues.length).toBe(1);
+ expect(list.issues[0].id).toBe(1);
+ done();
+ }, 0);
+ });
+
+ it('persists new list', (done) => {
+ gl.issueBoards.BoardsStore.new({
+ title: 'Test',
+ type: 'label',
+ label: {
+ id: 1,
+ title: 'Testing',
+ color: 'red',
+ description: 'testing;'
+ }
+ });
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+
+ setTimeout(() => {
+ const list = gl.issueBoards.BoardsStore.findList('id', 1);
+ expect(list).toBeDefined();
+ expect(list.id).toBe(1);
+ expect(list.position).toBe(0);
+ done();
+ }, 0);
+ });
+
+ it('check for blank state adding', () => {
+ expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
+ });
+
+ it('check for blank state not adding', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false);
+ });
+
+ it('check for blank state adding when backlog & done list exist', () => {
+ gl.issueBoards.BoardsStore.addList({
+ list_type: 'backlog'
+ });
+ gl.issueBoards.BoardsStore.addList({
+ list_type: 'done'
+ });
+
+ expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
+ });
+
+ it('adds the blank state', () => {
+ gl.issueBoards.BoardsStore.addBlankState();
+
+ const list = gl.issueBoards.BoardsStore.findList('type', 'blank', 'blank');
+ expect(list).toBeDefined();
+ });
+
+ it('removes list from state', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+
+ gl.issueBoards.BoardsStore.removeList(1, 'label');
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
+ });
+
+ it('moves the position of lists', () => {
+ const listOne = gl.issueBoards.BoardsStore.addList(listObj),
+ listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+
+ gl.issueBoards.BoardsStore.moveList(listOne, ['2', '1']);
+
+ expect(listOne.position).toBe(1);
+ });
+
+ it('moves an issue from one list to another', (done) => {
+ const listOne = gl.issueBoards.BoardsStore.addList(listObj),
+ listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+
+ setTimeout(() => {
+ expect(listOne.issues.length).toBe(1);
+ expect(listTwo.issues.length).toBe(1);
+
+ gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(1));
+
+ expect(listOne.issues.length).toBe(0);
+ expect(listTwo.issues.length).toBe(1);
+
+ done();
+ }, 0);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6
new file mode 100644
index 00000000000..3569d1b98bd
--- /dev/null
+++ b/spec/javascripts/boards/issue_spec.js.es6
@@ -0,0 +1,83 @@
+//= require jquery
+//= require jquery_ujs
+//= require jquery.cookie
+//= require vue
+//= require vue-resource
+//= require lib/utils/url_utility
+//= require boards/models/issue
+//= require boards/models/label
+//= require boards/models/list
+//= require boards/models/user
+//= require boards/services/board_service
+//= require boards/stores/boards_store
+//= require ./mock_data
+
+describe('Issue model', () => {
+ let issue;
+
+ beforeEach(() => {
+ gl.boardService = new BoardService('/test/issue-boards/board');
+ gl.issueBoards.BoardsStore.create();
+
+ issue = new ListIssue({
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ labels: [{
+ id: 1,
+ title: 'test',
+ color: 'red',
+ description: 'testing'
+ }]
+ });
+ });
+
+ it('has label', () => {
+ expect(issue.labels.length).toBe(1);
+ });
+
+ it('add new label', () => {
+ issue.addLabel({
+ id: 2,
+ title: 'bug',
+ color: 'blue',
+ description: 'bugs!'
+ });
+ expect(issue.labels.length).toBe(2);
+ });
+
+ it('does not add existing label', () => {
+ issue.addLabel({
+ id: 2,
+ title: 'test',
+ color: 'blue',
+ description: 'bugs!'
+ });
+
+ expect(issue.labels.length).toBe(1);
+ });
+
+ it('finds label', () => {
+ const label = issue.findLabel(issue.labels[0]);
+ expect(label).toBeDefined();
+ });
+
+ it('removes label', () => {
+ const label = issue.findLabel(issue.labels[0]);
+ issue.removeLabel(label);
+ expect(issue.labels.length).toBe(0);
+ });
+
+ it('removes multiple labels', () => {
+ issue.addLabel({
+ id: 2,
+ title: 'bug',
+ color: 'blue',
+ description: 'bugs!'
+ });
+ expect(issue.labels.length).toBe(2);
+
+ issue.removeLabels([issue.labels[0], issue.labels[1]]);
+ expect(issue.labels.length).toBe(0);
+ });
+});
diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6
new file mode 100644
index 00000000000..c206b794442
--- /dev/null
+++ b/spec/javascripts/boards/list_spec.js.es6
@@ -0,0 +1,89 @@
+//= require jquery
+//= require jquery_ujs
+//= require jquery.cookie
+//= require vue
+//= require vue-resource
+//= require lib/utils/url_utility
+//= require boards/models/issue
+//= require boards/models/label
+//= require boards/models/list
+//= require boards/models/user
+//= require boards/services/board_service
+//= require boards/stores/boards_store
+//= require ./mock_data
+
+describe('List model', () => {
+ let list;
+
+ beforeEach(() => {
+ gl.boardService = new BoardService('/test/issue-boards/board');
+ gl.issueBoards.BoardsStore.create();
+
+ list = new List(listObj);
+ });
+
+ it('gets issues when created', (done) => {
+ setTimeout(() => {
+ expect(list.issues.length).toBe(1);
+ done();
+ }, 0);
+ });
+
+ it('saves list and returns ID', (done) => {
+ list = new List({
+ title: 'test',
+ label: {
+ id: 1,
+ title: 'test',
+ color: 'red'
+ }
+ });
+ list.save();
+
+ setTimeout(() => {
+ expect(list.id).toBe(1);
+ expect(list.type).toBe('label');
+ expect(list.position).toBe(0);
+ done();
+ }, 0);
+ });
+
+ it('destroys the list', (done) => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ list = gl.issueBoards.BoardsStore.findList('id', 1);
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+ list.destroy();
+
+ setTimeout(() => {
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
+ done();
+ }, 0);
+ });
+
+ it('can\'t search when not backlog', () => {
+ expect(list.canSearch()).toBe(false);
+ });
+
+ it('can search when backlog', () => {
+ list.type = 'backlog';
+ expect(list.canSearch()).toBe(true);
+ });
+
+ it('gets issue from list', (done) => {
+ setTimeout(() => {
+ const issue = list.findIssue(1);
+ expect(issue).toBeDefined();
+ done();
+ }, 0);
+ });
+
+ it('removes issue', (done) => {
+ setTimeout(() => {
+ const issue = list.findIssue(1);
+ expect(list.issues.length).toBe(1);
+ list.removeIssue(issue);
+ expect(list.issues.length).toBe(0);
+ done();
+ }, 0);
+ });
+});
diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6
new file mode 100644
index 00000000000..0c37ec8354f
--- /dev/null
+++ b/spec/javascripts/boards/mock_data.js.es6
@@ -0,0 +1,53 @@
+const listObj = {
+ id: 1,
+ position: 0,
+ title: 'Test',
+ list_type: 'label',
+ label: {
+ id: 1,
+ title: 'Testing',
+ color: 'red',
+ description: 'testing;'
+ }
+};
+
+const listObjDuplicate = {
+ id: 2,
+ position: 1,
+ title: 'Test',
+ list_type: 'label',
+ label: {
+ id: 2,
+ title: 'Testing',
+ color: 'red',
+ description: 'testing;'
+ }
+};
+
+const BoardsMockData = {
+ 'GET': {
+ '/test/issue-boards/board/lists{/id}/issues': [{
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ labels: []
+ }]
+ },
+ 'POST': {
+ '/test/issue-boards/board/lists{/id}': listObj
+ },
+ 'PUT': {
+ '/test/issue-boards/board/lists{/id}': {}
+ },
+ 'DELETE': {
+ '/test/issue-boards/board/lists{/id}': {}
+ }
+};
+
+Vue.http.interceptors.push((request, next) => {
+ const body = BoardsMockData[request.method][request.url];
+
+ next(request.respondWith(JSON.stringify(body), {
+ status: 200
+ }));
+});
diff --git a/spec/javascripts/fixtures/abuse_reports.html.haml b/spec/javascripts/fixtures/abuse_reports.html.haml
new file mode 100644
index 00000000000..2ec302abcb7
--- /dev/null
+++ b/spec/javascripts/fixtures/abuse_reports.html.haml
@@ -0,0 +1,16 @@
+.abuse-reports
+ .message#long
+ Cat ipsum dolor sit amet, hide head under blanket so no one can see.
+ Gate keepers of hell eat and than sleep on your face but hunt by meowing
+ loudly at 5am next to human slave food dispenser cats go for world
+ domination or chase laser, yet poop on grasses chirp at birds. Cat is love,
+ cat is life chase after silly colored fish toys around the house climb a
+ tree, wait for a fireman jump to fireman then scratch his face fall asleep
+ on the washing machine lies down always hungry so caticus cuteicus. Sit on
+ human. Spot something, big eyes, big eyes, crouch, shake butt, prepare to
+ pounce sleep in the bathroom sink hiss at vacuum cleaner hide head under
+ blanket so no one can see throwup on your pillow.
+ .message#short
+ Cat ipsum dolor sit amet, groom yourself 4 hours - checked, have your
+ beauty sleep 18 hours - checked, be fabulous for the rest of the day -
+ checked! for shake treat bag.
diff --git a/spec/lib/gitlab/conflict/file_collection_spec.rb b/spec/lib/gitlab/conflict/file_collection_spec.rb
new file mode 100644
index 00000000000..39d892c18c0
--- /dev/null
+++ b/spec/lib/gitlab/conflict/file_collection_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Gitlab::Conflict::FileCollection, lib: true do
+ let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start') }
+ let(:file_collection) { Gitlab::Conflict::FileCollection.new(merge_request) }
+
+ describe '#files' do
+ it 'returns an array of Conflict::Files' do
+ expect(file_collection.files).to all(be_an_instance_of(Gitlab::Conflict::File))
+ end
+ end
+
+ describe '#default_commit_message' do
+ it 'matches the format of the git CLI commit message' do
+ expect(file_collection.default_commit_message).to eq(<<EOM.chomp)
+Merge branch 'conflict-start' into 'conflict-resolvable'
+
+# Conflicts:
+# files/ruby/popen.rb
+# files/ruby/regex.rb
+EOM
+ end
+ end
+end
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
new file mode 100644
index 00000000000..60020487061
--- /dev/null
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -0,0 +1,261 @@
+require 'spec_helper'
+
+describe Gitlab::Conflict::File, lib: true do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+ let(:rugged) { repository.rugged }
+ let(:their_commit) { rugged.branches['conflict-start'].target }
+ let(:our_commit) { rugged.branches['conflict-resolvable'].target }
+ let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) }
+ let(:index) { rugged.merge_commits(our_commit, their_commit) }
+ let(:conflict) { index.conflicts.last }
+ let(:merge_file_result) { index.merge_file('files/ruby/regex.rb') }
+ let(:conflict_file) { Gitlab::Conflict::File.new(merge_file_result, conflict, merge_request: merge_request) }
+
+ describe '#resolve_lines' do
+ let(:section_keys) { conflict_file.sections.map { |section| section[:id] }.compact }
+
+ context 'when resolving everything to the same side' do
+ let(:resolution_hash) { section_keys.map { |key| [key, 'head'] }.to_h }
+ let(:resolved_lines) { conflict_file.resolve_lines(resolution_hash) }
+ let(:expected_lines) { conflict_file.lines.reject { |line| line.type == 'old' } }
+
+ it 'has the correct number of lines' do
+ expect(resolved_lines.length).to eq(expected_lines.length)
+ end
+
+ it 'has content matching the chosen lines' do
+ expect(resolved_lines.map(&:text)).to eq(expected_lines.map(&:text))
+ end
+ end
+
+ context 'with mixed resolutions' do
+ let(:resolution_hash) do
+ section_keys.map.with_index { |key, i| [key, i.even? ? 'head' : 'origin'] }.to_h
+ end
+
+ let(:resolved_lines) { conflict_file.resolve_lines(resolution_hash) }
+
+ it 'has the correct number of lines' do
+ file_lines = conflict_file.lines.reject { |line| line.type == 'new' }
+
+ expect(resolved_lines.length).to eq(file_lines.length)
+ end
+
+ it 'returns a file containing only the chosen parts of the resolved sections' do
+ expect(resolved_lines.chunk { |line| line.type || 'both' }.map(&:first)).
+ to eq(['both', 'new', 'both', 'old', 'both', 'new', 'both'])
+ end
+ end
+
+ it 'raises MissingResolution when passed a hash without resolutions for all sections' do
+ empty_hash = section_keys.map { |key| [key, nil] }.to_h
+ invalid_hash = section_keys.map { |key| [key, 'invalid'] }.to_h
+
+ expect { conflict_file.resolve_lines({}) }.
+ to raise_error(Gitlab::Conflict::File::MissingResolution)
+
+ expect { conflict_file.resolve_lines(empty_hash) }.
+ to raise_error(Gitlab::Conflict::File::MissingResolution)
+
+ expect { conflict_file.resolve_lines(invalid_hash) }.
+ to raise_error(Gitlab::Conflict::File::MissingResolution)
+ end
+ end
+
+ describe '#highlight_lines!' do
+ def html_to_text(html)
+ CGI.unescapeHTML(ActionView::Base.full_sanitizer.sanitize(html)).delete("\n")
+ end
+
+ it 'modifies the existing lines' do
+ expect { conflict_file.highlight_lines! }.to change { conflict_file.lines.map(&:instance_variables) }
+ end
+
+ it 'is called implicitly when rich_text is accessed on a line' do
+ expect(conflict_file).to receive(:highlight_lines!).once.and_call_original
+
+ conflict_file.lines.each(&:rich_text)
+ end
+
+ it 'sets the rich_text of the lines matching the text content' do
+ conflict_file.lines.each do |line|
+ expect(line.text).to eq(html_to_text(line.rich_text))
+ end
+ end
+ end
+
+ describe '#sections' do
+ it 'only inserts match lines when there is a gap between sections' do
+ conflict_file.sections.each_with_index do |section, i|
+ previous_line_number = 0
+ current_line_number = section[:lines].map(&:old_line).compact.min
+
+ if i > 0
+ previous_line_number = conflict_file.sections[i - 1][:lines].map(&:old_line).compact.last
+ end
+
+ if current_line_number == previous_line_number + 1
+ expect(section[:lines].first.type).not_to eq('match')
+ else
+ expect(section[:lines].first.type).to eq('match')
+ expect(section[:lines].first.text).to match(/\A@@ -#{current_line_number},\d+ \+\d+,\d+ @@ module Gitlab\Z/)
+ end
+ end
+ end
+
+ it 'sets conflict to false for sections with only unchanged lines' do
+ conflict_file.sections.reject { |section| section[:conflict] }.each do |section|
+ without_match = section[:lines].reject { |line| line.type == 'match' }
+
+ expect(without_match).to all(have_attributes(type: nil))
+ end
+ end
+
+ it 'only includes a maximum of CONTEXT_LINES (plus an optional match line) in context sections' do
+ conflict_file.sections.reject { |section| section[:conflict] }.each do |section|
+ without_match = section[:lines].reject { |line| line.type == 'match' }
+
+ expect(without_match.length).to be <= Gitlab::Conflict::File::CONTEXT_LINES * 2
+ end
+ end
+
+ it 'sets conflict to true for sections with only changed lines' do
+ conflict_file.sections.select { |section| section[:conflict] }.each do |section|
+ section[:lines].each do |line|
+ expect(line.type).to be_in(['new', 'old'])
+ end
+ end
+ end
+
+ it 'adds unique IDs to conflict sections, and not to other sections' do
+ section_ids = []
+
+ conflict_file.sections.each do |section|
+ if section[:conflict]
+ expect(section).to have_key(:id)
+ section_ids << section[:id]
+ else
+ expect(section).not_to have_key(:id)
+ end
+ end
+
+ expect(section_ids.uniq).to eq(section_ids)
+ end
+
+ context 'with an example file' do
+ let(:file) do
+ <<FILE
+ # Ensure there is no match line header here
+ def username_regexp
+ default_regexp
+ end
+
+<<<<<<< files/ruby/regex.rb
+def project_name_regexp
+ /\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z/
+end
+
+def name_regexp
+ /\A[a-zA-Z0-9_\-\. ]*\z/
+=======
+def project_name_regex
+ %r{\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z}
+end
+
+def name_regex
+ %r{\A[a-zA-Z0-9_\-\. ]*\z}
+>>>>>>> files/ruby/regex.rb
+end
+
+# Some extra lines
+# To force a match line
+# To be created
+
+def path_regexp
+ default_regexp
+end
+
+<<<<<<< files/ruby/regex.rb
+def archive_formats_regexp
+ /(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/
+=======
+def archive_formats_regex
+ %r{(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)}
+>>>>>>> files/ruby/regex.rb
+end
+
+def git_reference_regexp
+ # Valid git ref regexp, see:
+ # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
+ %r{
+ (?!
+ (?# doesn't begins with)
+ \/| (?# rule #6)
+ (?# doesn't contain)
+ .*(?:
+ [\/.]\.| (?# rule #1,3)
+ \/\/| (?# rule #6)
+ @\{| (?# rule #8)
+ \\ (?# rule #9)
+ )
+ )
+ [^\000-\040\177~^:?*\[]+ (?# rule #4-5)
+ (?# doesn't end with)
+ (?<!\.lock) (?# rule #1)
+ (?<![\/.]) (?# rule #6-7)
+ }x
+end
+
+protected
+
+<<<<<<< files/ruby/regex.rb
+def default_regexp
+ /\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z/
+=======
+def default_regex
+ %r{\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z}
+>>>>>>> files/ruby/regex.rb
+end
+FILE
+ end
+
+ let(:conflict_file) { Gitlab::Conflict::File.new({ data: file }, conflict, merge_request: merge_request) }
+ let(:sections) { conflict_file.sections }
+
+ it 'sets the correct match line headers' do
+ expect(sections[0][:lines].first).to have_attributes(type: 'match', text: '@@ -3,14 +3,14 @@')
+ expect(sections[3][:lines].first).to have_attributes(type: 'match', text: '@@ -19,26 +19,26 @@ def path_regexp')
+ expect(sections[6][:lines].first).to have_attributes(type: 'match', text: '@@ -47,52 +47,52 @@ end')
+ end
+
+ it 'does not add match lines where they are not needed' do
+ expect(sections[1][:lines].first.type).not_to eq('match')
+ expect(sections[2][:lines].first.type).not_to eq('match')
+ expect(sections[4][:lines].first.type).not_to eq('match')
+ expect(sections[5][:lines].first.type).not_to eq('match')
+ expect(sections[7][:lines].first.type).not_to eq('match')
+ end
+
+ it 'creates context sections of the correct length' do
+ expect(sections[0][:lines].reject(&:type).length).to eq(3)
+ expect(sections[2][:lines].reject(&:type).length).to eq(3)
+ expect(sections[3][:lines].reject(&:type).length).to eq(3)
+ expect(sections[5][:lines].reject(&:type).length).to eq(3)
+ expect(sections[6][:lines].reject(&:type).length).to eq(3)
+ expect(sections[8][:lines].reject(&:type).length).to eq(1)
+ end
+ end
+ end
+
+ describe '#as_json' do
+ it 'includes the blob path for the file' do
+ expect(conflict_file.as_json[:blob_path]).
+ to eq("/#{project.namespace.to_param}/#{merge_request.project.to_param}/blob/#{our_commit.oid}/files/ruby/regex.rb")
+ end
+
+ it 'includes the blob icon for the file' do
+ expect(conflict_file.as_json[:blob_icon]).to eq('file-text-o')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/conflict/parser_spec.rb b/spec/lib/gitlab/conflict/parser_spec.rb
new file mode 100644
index 00000000000..65a828accde
--- /dev/null
+++ b/spec/lib/gitlab/conflict/parser_spec.rb
@@ -0,0 +1,188 @@
+require 'spec_helper'
+
+describe Gitlab::Conflict::Parser, lib: true do
+ let(:parser) { Gitlab::Conflict::Parser.new }
+
+ describe '#parse' do
+ def parse_text(text)
+ parser.parse(text, our_path: 'README.md', their_path: 'README.md')
+ end
+
+ context 'when the file has valid conflicts' do
+ let(:text) do
+ <<CONFLICT
+module Gitlab
+ module Regexp
+ extend self
+
+ def username_regexp
+ default_regexp
+ end
+
+<<<<<<< files/ruby/regex.rb
+ def project_name_regexp
+ /\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z/
+ end
+
+ def name_regexp
+ /\A[a-zA-Z0-9_\-\. ]*\z/
+=======
+ def project_name_regex
+ %r{\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z}
+ end
+
+ def name_regex
+ %r{\A[a-zA-Z0-9_\-\. ]*\z}
+>>>>>>> files/ruby/regex.rb
+ end
+
+ def path_regexp
+ default_regexp
+ end
+
+<<<<<<< files/ruby/regex.rb
+ def archive_formats_regexp
+ /(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/
+=======
+ def archive_formats_regex
+ %r{(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)}
+>>>>>>> files/ruby/regex.rb
+ end
+
+ def git_reference_regexp
+ # Valid git ref regexp, see:
+ # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
+ %r{
+ (?!
+ (?# doesn't begins with)
+ \/| (?# rule #6)
+ (?# doesn't contain)
+ .*(?:
+ [\/.]\.| (?# rule #1,3)
+ \/\/| (?# rule #6)
+ @\{| (?# rule #8)
+ \\ (?# rule #9)
+ )
+ )
+ [^\000-\040\177~^:?*\[]+ (?# rule #4-5)
+ (?# doesn't end with)
+ (?<!\.lock) (?# rule #1)
+ (?<![\/.]) (?# rule #6-7)
+ }x
+ end
+
+ protected
+
+<<<<<<< files/ruby/regex.rb
+ def default_regexp
+ /\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z/
+=======
+ def default_regex
+ %r{\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z}
+>>>>>>> files/ruby/regex.rb
+ end
+ end
+end
+CONFLICT
+ end
+
+ let(:lines) do
+ parser.parse(text, our_path: 'files/ruby/regex.rb', their_path: 'files/ruby/regex.rb')
+ end
+
+ it 'sets our lines as new lines' do
+ expect(lines[8..13]).to all(have_attributes(type: 'new'))
+ expect(lines[26..27]).to all(have_attributes(type: 'new'))
+ expect(lines[56..57]).to all(have_attributes(type: 'new'))
+ end
+
+ it 'sets their lines as old lines' do
+ expect(lines[14..19]).to all(have_attributes(type: 'old'))
+ expect(lines[28..29]).to all(have_attributes(type: 'old'))
+ expect(lines[58..59]).to all(have_attributes(type: 'old'))
+ end
+
+ it 'sets non-conflicted lines as both' do
+ expect(lines[0..7]).to all(have_attributes(type: nil))
+ expect(lines[20..25]).to all(have_attributes(type: nil))
+ expect(lines[30..55]).to all(have_attributes(type: nil))
+ expect(lines[60..62]).to all(have_attributes(type: nil))
+ end
+
+ it 'sets consecutive line numbers for index, old_pos, and new_pos' do
+ old_line_numbers = lines.select { |line| line.type != 'new' }.map(&:old_pos)
+ new_line_numbers = lines.select { |line| line.type != 'old' }.map(&:new_pos)
+
+ expect(lines.map(&:index)).to eq(0.upto(62).to_a)
+ expect(old_line_numbers).to eq(1.upto(53).to_a)
+ expect(new_line_numbers).to eq(1.upto(53).to_a)
+ end
+ end
+
+ context 'when the file contents include conflict delimiters' do
+ it 'raises UnexpectedDelimiter when there is a non-start delimiter first' do
+ expect { parse_text('=======') }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ expect { parse_text('>>>>>>> README.md') }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ expect { parse_text('>>>>>>> some-other-path.md') }.
+ not_to raise_error
+ end
+
+ it 'raises UnexpectedDelimiter when a start delimiter is followed by a non-middle delimiter' do
+ start_text = "<<<<<<< README.md\n"
+ end_text = "\n=======\n>>>>>>> README.md"
+
+ expect { parse_text(start_text + '>>>>>>> README.md' + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ expect { parse_text(start_text + start_text + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
+ not_to raise_error
+ end
+
+ it 'raises UnexpectedDelimiter when a middle delimiter is followed by a non-end delimiter' do
+ start_text = "<<<<<<< README.md\n=======\n"
+ end_text = "\n>>>>>>> README.md"
+
+ expect { parse_text(start_text + '=======' + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ expect { parse_text(start_text + start_text + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
+ not_to raise_error
+ end
+
+ it 'raises MissingEndDelimiter when there is no end delimiter at the end' do
+ start_text = "<<<<<<< README.md\n=======\n"
+
+ expect { parse_text(start_text) }.
+ to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter)
+
+ expect { parse_text(start_text + '>>>>>>> some-other-path.md') }.
+ to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter)
+ end
+ end
+
+ context 'other file types' do
+ it 'raises UnmergeableFile when lines is blank, indicating a binary file' do
+ expect { parse_text('') }.
+ to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
+
+ expect { parse_text(nil) }.
+ to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
+ end
+
+ it 'raises UnmergeableFile when the file is over 100 KB' do
+ expect { parse_text('a' * 102401) }.
+ to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/downtime_check/message_spec.rb b/spec/lib/gitlab/downtime_check/message_spec.rb
index 93094cda776..a5a398abf78 100644
--- a/spec/lib/gitlab/downtime_check/message_spec.rb
+++ b/spec/lib/gitlab/downtime_check/message_spec.rb
@@ -5,13 +5,35 @@ describe Gitlab::DowntimeCheck::Message do
it 'returns an ANSI formatted String for an offline migration' do
message = described_class.new('foo.rb', true, 'hello')
- expect(message.to_s).to eq("[\e[32moffline\e[0m]: foo.rb: hello")
+ expect(message.to_s).to eq("[\e[31moffline\e[0m]: foo.rb:\n\nhello\n\n")
end
it 'returns an ANSI formatted String for an online migration' do
message = described_class.new('foo.rb')
- expect(message.to_s).to eq("[\e[31monline\e[0m]: foo.rb")
+ expect(message.to_s).to eq("[\e[32monline\e[0m]: foo.rb")
+ end
+ end
+
+ describe '#reason?' do
+ it 'returns false when no reason is specified' do
+ message = described_class.new('foo.rb')
+
+ expect(message.reason?).to eq(false)
+ end
+
+ it 'returns true when a reason is specified' do
+ message = described_class.new('foo.rb', true, 'hello')
+
+ expect(message.reason?).to eq(true)
+ end
+ end
+
+ describe '#reason' do
+ it 'strips excessive whitespace from the returned String' do
+ message = described_class.new('foo.rb', true, " hello\n world\n\n foo")
+
+ expect(message.reason).to eq("hello\nworld\n\nfoo")
end
end
end
diff --git a/spec/lib/gitlab/metrics/metric_spec.rb b/spec/lib/gitlab/metrics/metric_spec.rb
index f718d536130..f26fca52c50 100644
--- a/spec/lib/gitlab/metrics/metric_spec.rb
+++ b/spec/lib/gitlab/metrics/metric_spec.rb
@@ -23,6 +23,24 @@ describe Gitlab::Metrics::Metric do
it { is_expected.to eq({ host: 'localtoast' }) }
end
+ describe '#type' do
+ subject { metric.type }
+
+ it { is_expected.to eq(:metric) }
+ end
+
+ describe '#event?' do
+ it 'returns false for a regular metric' do
+ expect(metric.event?).to eq(false)
+ end
+
+ it 'returns true for an event metric' do
+ expect(metric).to receive(:type).and_return(:event)
+
+ expect(metric.event?).to eq(true)
+ end
+ end
+
describe '#to_hash' do
it 'returns a Hash' do
expect(metric.to_hash).to be_an_instance_of(Hash)
diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
index f264ed64029..a30cb2a5e38 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -45,6 +45,15 @@ describe Gitlab::Metrics::RackMiddleware do
middleware.call(env)
end
+
+ it 'tracks any raised exceptions' do
+ expect(app).to receive(:call).with(env).and_raise(RuntimeError)
+
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ to receive(:add_event).with(:rails_exception)
+
+ expect { middleware.call(env) }.to raise_error(RuntimeError)
+ end
end
describe '#transaction_from_env' do
diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
index 4d2aa03e722..acaba785606 100644
--- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
@@ -12,7 +12,9 @@ describe Gitlab::Metrics::SidekiqMiddleware do
with('TestWorker#perform').
and_call_original
- expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set).with(:sidekiq_queue_duration, instance_of(Float))
+ expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set).
+ with(:sidekiq_queue_duration, instance_of(Float))
+
expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
middleware.call(worker, message, :test) { nil }
@@ -25,10 +27,28 @@ describe Gitlab::Metrics::SidekiqMiddleware do
with('TestWorker#perform').
and_call_original
- expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set).with(:sidekiq_queue_duration, instance_of(Float))
+ expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set).
+ with(:sidekiq_queue_duration, instance_of(Float))
+
expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
middleware.call(worker, {}, :test) { nil }
end
+
+ it 'tracks any raised exceptions' do
+ worker = double(:worker, class: double(:class, name: 'TestWorker'))
+
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ to receive(:run).and_raise(RuntimeError)
+
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ to receive(:add_event).with(:sidekiq_exception)
+
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ to receive(:finish)
+
+ expect { middleware.call(worker, message, :test) }.
+ to raise_error(RuntimeError)
+ end
end
end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index f1a191d9410..3887c04c832 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -142,5 +142,62 @@ describe Gitlab::Metrics::Transaction do
transaction.submit
end
+
+ it 'does not add an action tag for events' do
+ transaction.action = 'Foo#bar'
+ transaction.add_event(:meow)
+
+ hash = {
+ series: 'events',
+ tags: { event: :meow },
+ values: { count: 1 },
+ timestamp: an_instance_of(Fixnum)
+ }
+
+ expect(Gitlab::Metrics).to receive(:submit_metrics).
+ with([hash])
+
+ transaction.submit
+ end
+ end
+
+ describe '#add_event' do
+ it 'adds a metric' do
+ transaction.add_event(:meow)
+
+ expect(transaction.metrics[0]).to be_an_instance_of(Gitlab::Metrics::Metric)
+ end
+
+ it "does not prefix the metric's series name" do
+ transaction.add_event(:meow)
+
+ metric = transaction.metrics[0]
+
+ expect(metric.series).to eq(described_class::EVENT_SERIES)
+ end
+
+ it 'tracks a counter for every event' do
+ transaction.add_event(:meow)
+
+ metric = transaction.metrics[0]
+
+ expect(metric.values).to eq(count: 1)
+ end
+
+ it 'tracks the event name' do
+ transaction.add_event(:meow)
+
+ metric = transaction.metrics[0]
+
+ expect(metric.tags).to eq(event: :meow)
+ end
+
+ it 'allows tracking of custom tags' do
+ transaction.add_event(:meow, animal: 'cat')
+
+ metric = transaction.metrics[0]
+
+ expect(metric.tags).to eq(event: :meow, animal: 'cat')
+ end
end
end
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
index 84f9475a0f8..ab6e311b1e8 100644
--- a/spec/lib/gitlab/metrics_spec.rb
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -153,4 +153,28 @@ describe Gitlab::Metrics do
expect(described_class.series_prefix).to be_an_instance_of(String)
end
end
+
+ describe '.add_event' do
+ context 'without a transaction' do
+ it 'does nothing' do
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ not_to receive(:add_event)
+
+ Gitlab::Metrics.add_event(:meow)
+ end
+ end
+
+ context 'with a transaction' do
+ it 'adds an event' do
+ transaction = Gitlab::Metrics::Transaction.new
+
+ expect(transaction).to receive(:add_event).with(:meow)
+
+ expect(Gitlab::Metrics).to receive(:current_transaction).
+ and_return(transaction)
+
+ Gitlab::Metrics.add_event(:meow)
+ end
+ end
+ end
end
diff --git a/spec/models/board_spec.rb b/spec/models/board_spec.rb
new file mode 100644
index 00000000000..12d29540137
--- /dev/null
+++ b/spec/models/board_spec.rb
@@ -0,0 +1,12 @@
+require 'rails_helper'
+
+describe Board do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_many(:lists).order(list_type: :asc, position: :asc).dependent(:delete_all) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ end
+end
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index 2a09063f857..5a5d1a5d60c 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -5,8 +5,10 @@ describe Label, models: true do
describe 'associations' do
it { is_expected.to belong_to(:project) }
+
it { is_expected.to have_many(:label_links).dependent(:destroy) }
it { is_expected.to have_many(:issues).through(:label_links).source(:target) }
+ it { is_expected.to have_many(:lists).dependent(:destroy) }
end
describe 'modules' do
diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb
new file mode 100644
index 00000000000..9e1a52011c3
--- /dev/null
+++ b/spec/models/list_spec.rb
@@ -0,0 +1,117 @@
+require 'rails_helper'
+
+describe List do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:board) }
+ it { is_expected.to belong_to(:label) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:board) }
+ it { is_expected.to validate_presence_of(:label) }
+ it { is_expected.to validate_presence_of(:list_type) }
+ it { is_expected.to validate_presence_of(:position) }
+ it { is_expected.to validate_numericality_of(:position).only_integer.is_greater_than_or_equal_to(0) }
+
+ it 'validates uniqueness of label scoped to board_id' do
+ create(:list)
+
+ expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:board_id)
+ end
+
+ context 'when list_type is set to backlog' do
+ subject { described_class.new(list_type: :backlog) }
+
+ it { is_expected.not_to validate_presence_of(:label) }
+ it { is_expected.not_to validate_presence_of(:position) }
+ end
+
+ context 'when list_type is set to done' do
+ subject { described_class.new(list_type: :done) }
+
+ it { is_expected.not_to validate_presence_of(:label) }
+ it { is_expected.not_to validate_presence_of(:position) }
+ end
+ end
+
+ describe '#destroy' do
+ it 'can be destroyed when when list_type is set to label' do
+ subject = create(:list)
+
+ expect(subject.destroy).to be_truthy
+ end
+
+ it 'can not be destroyed when list_type is set to backlog' do
+ subject = create(:backlog_list)
+
+ expect(subject.destroy).to be_falsey
+ end
+
+ it 'can not be destroyed when when list_type is set to done' do
+ subject = create(:done_list)
+
+ expect(subject.destroy).to be_falsey
+ end
+ end
+
+ describe '#destroyable?' do
+ it 'retruns true when list_type is set to label' do
+ subject.list_type = :label
+
+ expect(subject).to be_destroyable
+ end
+
+ it 'retruns false when list_type is set to backlog' do
+ subject.list_type = :backlog
+
+ expect(subject).not_to be_destroyable
+ end
+
+ it 'retruns false when list_type is set to done' do
+ subject.list_type = :done
+
+ expect(subject).not_to be_destroyable
+ end
+ end
+
+ describe '#movable?' do
+ it 'retruns true when list_type is set to label' do
+ subject.list_type = :label
+
+ expect(subject).to be_movable
+ end
+
+ it 'retruns false when list_type is set to backlog' do
+ subject.list_type = :backlog
+
+ expect(subject).not_to be_movable
+ end
+
+ it 'retruns false when list_type is set to done' do
+ subject.list_type = :done
+
+ expect(subject).not_to be_movable
+ end
+ end
+
+ describe '#title' do
+ it 'returns label name when list_type is set to label' do
+ subject.list_type = :label
+ subject.label = Label.new(name: 'Development')
+
+ expect(subject.title).to eq 'Development'
+ end
+
+ it 'returns Backlog when list_type is set to backlog' do
+ subject.list_type = :backlog
+
+ expect(subject.title).to eq 'Backlog'
+ end
+
+ it 'returns Done when list_type is set to done' do
+ subject.list_type = :done
+
+ expect(subject.title).to eq 'Done'
+ end
+ end
+end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 35a4418ebb3..f83dbefedc0 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -456,6 +456,20 @@ describe MergeRequest, models: true do
subject { create :merge_request, :simple }
end
+ describe '#commits_sha' do
+ let(:commit0) { double('commit0', sha: 'sha1') }
+ let(:commit1) { double('commit1', sha: 'sha2') }
+ let(:commit2) { double('commit2', sha: 'sha3') }
+
+ before do
+ allow(subject.merge_request_diff).to receive(:commits).and_return([commit0, commit1, commit2])
+ end
+
+ it 'returns sha of commits' do
+ expect(subject.commits_sha).to contain_exactly('sha1', 'sha2', 'sha3')
+ end
+ end
+
describe '#pipeline' do
describe 'when the source project exists' do
it 'returns the latest pipeline' do
@@ -480,6 +494,19 @@ describe MergeRequest, models: true do
end
end
+ describe '#all_pipelines' do
+ let!(:pipelines) do
+ subject.merge_request_diff.commits.map do |commit|
+ create(:ci_empty_pipeline, project: subject.source_project, sha: commit.id, ref: subject.source_branch)
+ end
+ end
+
+ it 'returns a pipelines from source projects with proper ordering' do
+ expect(subject.all_pipelines).not_to be_empty
+ expect(subject.all_pipelines).to eq(pipelines.reverse)
+ end
+ end
+
describe '#participants' do
let(:project) { create(:project, :public) }
@@ -756,4 +783,56 @@ describe MergeRequest, models: true do
end
end
end
+
+ describe '#conflicts_can_be_resolved_in_ui?' do
+ def create_merge_request(source_branch)
+ create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start') do |mr|
+ mr.mark_as_unmergeable
+ end
+ end
+
+ it 'returns a falsey value when the MR can be merged without conflicts' do
+ merge_request = create_merge_request('master')
+ merge_request.mark_as_mergeable
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the MR does not support new diff notes' do
+ merge_request = create_merge_request('conflict-resolvable')
+ merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the conflicts contain a large file' do
+ merge_request = create_merge_request('conflict-too-large')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the conflicts contain a binary file' do
+ merge_request = create_merge_request('conflict-binary-file')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the conflicts contain a file with ambiguous conflict markers' do
+ merge_request = create_merge_request('conflict-contains-conflict-markers')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the conflicts contain a file edited in one branch and deleted in another' do
+ merge_request = create_merge_request('conflict-missing-side')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a truthy value when the conflicts are resolvable in the UI' do
+ merge_request = create_merge_request('conflict-resolvable')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 0a32a486703..d1f3a815290 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -23,6 +23,7 @@ describe Project, models: true do
it { is_expected.to have_one(:slack_service).dependent(:destroy) }
it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
it { is_expected.to have_one(:asana_service).dependent(:destroy) }
+ it { is_expected.to have_one(:board).dependent(:destroy) }
it { is_expected.to have_many(:commit_statuses) }
it { is_expected.to have_many(:pipelines) }
it { is_expected.to have_many(:builds) }
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index 41503885dd9..9a17a705b1e 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -407,4 +407,27 @@ describe API::API, api: true do
end
end
end
+
+ describe 'POST /projects/:id/builds/:build_id/play' do
+ before do
+ post api("/projects/#{project.id}/builds/#{build.id}/play", user)
+ end
+
+ context 'on an playable build' do
+ let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) }
+
+ it 'plays the build' do
+ expect(response).to have_http_status 200
+ expect(json_response['user']['id']).to eq(user.id)
+ expect(json_response['id']).to eq(build.id)
+ end
+ end
+
+ context 'on a non-playable build' do
+ it 'returns a status code 400, Bad Request' do
+ expect(response).to have_http_status 400
+ expect(response.body).to match("Unplayable Build")
+ end
+ end
+ end
end
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
new file mode 100644
index 00000000000..8fa8c66db6c
--- /dev/null
+++ b/spec/requests/api/deployments_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:project) { deployment.environment.project }
+ let!(:deployment) { create(:deployment) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe 'GET /projects/:id/deployments' do
+ context 'as member of the project' do
+ it_behaves_like 'a paginated resources' do
+ let(:request) { get api("/projects/#{project.id}/deployments", user) }
+ end
+
+ it 'returns projects deployments' do
+ get api("/projects/#{project.id}/deployments", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['iid']).to eq(deployment.iid)
+ expect(json_response.first['sha']).to match /\A\h{40}\z/
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ get api("/projects/#{project.id}/deployments", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/deployments/:deployment_id' do
+ context 'as a member of the project' do
+ it 'returns the projects deployment' do
+ get api("/projects/#{project.id}/deployments/#{deployment.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['sha']).to match /\A\h{40}\z/
+ expect(json_response['id']).to eq(deployment.id)
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 05e57905343..1898b07835d 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -26,6 +26,7 @@ describe API::API, api: true do
expect(json_response.size).to eq(1)
expect(json_response.first['name']).to eq(environment.name)
expect(json_response.first['external_url']).to eq(environment.external_url)
+ expect(json_response.first['project']['id']).to eq(project.id)
end
end
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
new file mode 100644
index 00000000000..7011bdc9ec0
--- /dev/null
+++ b/spec/requests/api/pipelines_spec.rb
@@ -0,0 +1,133 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:project) { create(:project, creator_id: user.id) }
+
+ let!(:pipeline) do
+ create(:ci_empty_pipeline, project: project, sha: project.commit.id,
+ ref: project.default_branch)
+ end
+
+ before { project.team << [user, :master] }
+
+ describe 'GET /projects/:id/pipelines ' do
+ it_behaves_like 'a paginated resources' do
+ let(:request) { get api("/projects/#{project.id}/pipelines", user) }
+ end
+
+ context 'authorized user' do
+ it 'returns project pipelines' do
+ get api("/projects/#{project.id}/pipelines", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['sha']).to match /\A\h{40}\z/
+ expect(json_response.first['id']).to eq pipeline.id
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not return project pipelines' do
+ get api("/projects/#{project.id}/pipelines", non_member)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(json_response).not_to be_an Array
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/pipelines/:pipeline_id' do
+ context 'authorized user' do
+ it 'returns project pipelines' do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['sha']).to match /\A\h{40}\z/
+ end
+
+ it 'returns 404 when it does not exist' do
+ get api("/projects/#{project.id}/pipelines/123456", user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq '404 Not found'
+ expect(json_response['id']).to be nil
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return a project pipeline' do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(json_response['id']).to be nil
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do
+ context 'authorized user' do
+ let!(:pipeline) do
+ create(:ci_pipeline, project: project, sha: project.commit.id,
+ ref: project.default_branch)
+ end
+
+ let!(:build) { create(:ci_build, :failed, pipeline: pipeline) }
+
+ it 'retries failed builds' do
+ expect do
+ post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user)
+ end.to change { pipeline.builds.count }.from(1).to(2)
+
+ expect(response).to have_http_status(201)
+ expect(build.reload.retried?).to be true
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return a project pipeline' do
+ post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(json_response['id']).to be nil
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do
+ let!(:pipeline) do
+ create(:ci_empty_pipeline, project: project, sha: project.commit.id,
+ ref: project.default_branch)
+ end
+
+ let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+ context 'authorized user' do
+ it 'retries failed builds' do
+ post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['status']).to eq('canceled')
+ end
+ end
+
+ context 'user without proper access rights' do
+ let!(:reporter) { create(:user) }
+
+ before { project.team << [reporter, :reporter] }
+
+ it 'rejects the action' do
+ post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter)
+
+ expect(response).to have_http_status(403)
+ expect(pipeline.reload.status).to eq('pending')
+ end
+ end
+ end
+end
diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb
new file mode 100644
index 00000000000..a1a4dd4c57c
--- /dev/null
+++ b/spec/services/boards/create_service_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Boards::CreateService, services: true do
+ describe '#execute' do
+ subject(:service) { described_class.new(project, double) }
+
+ context 'when project does not have a board' do
+ let(:project) { create(:empty_project, board: nil) }
+
+ it 'creates a new board' do
+ expect { service.execute }.to change(Board, :count).by(1)
+ end
+
+ it 'creates default lists' do
+ service.execute
+
+ expect(project.board.lists.size).to eq 2
+ expect(project.board.lists.first).to be_backlog
+ expect(project.board.lists.last).to be_done
+ end
+ end
+
+ context 'when project has a board' do
+ let!(:project) { create(:project_with_board) }
+
+ it 'does not create a new board' do
+ expect { service.execute }.not_to change(Board, :count)
+ end
+
+ it 'does not create board lists' do
+ expect { service.execute }.not_to change(project.board.lists, :count)
+ end
+ end
+ end
+end
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
new file mode 100644
index 00000000000..f7f45983d26
--- /dev/null
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe Boards::Issues::ListService, services: true do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+
+ let(:bug) { create(:label, project: project, name: 'Bug') }
+ let(:development) { create(:label, project: project, name: 'Development') }
+ let(:testing) { create(:label, project: project, name: 'Testing') }
+ let(:p1) { create(:label, title: 'P1', project: project, priority: 1) }
+ let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
+ let(:p3) { create(:label, title: 'P3', project: project, priority: 3) }
+
+ let!(:backlog) { create(:backlog_list, board: board) }
+ let!(:list1) { create(:list, board: board, label: development, position: 0) }
+ let!(:list2) { create(:list, board: board, label: testing, position: 1) }
+ let!(:done) { create(:done_list, board: board) }
+
+ let!(:opened_issue1) { create(:labeled_issue, project: project, labels: [bug]) }
+ let!(:opened_issue2) { create(:labeled_issue, project: project, labels: [p2]) }
+ let!(:reopened_issue1) { create(:issue, :reopened, project: project) }
+
+ let!(:list1_issue1) { create(:labeled_issue, project: project, labels: [p2, development]) }
+ let!(:list1_issue2) { create(:labeled_issue, project: project, labels: [development]) }
+ let!(:list1_issue3) { create(:labeled_issue, project: project, labels: [development, p1]) }
+ let!(:list2_issue1) { create(:labeled_issue, project: project, labels: [testing]) }
+
+ let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
+ let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) }
+ let!(:closed_issue3) { create(:issue, :closed, project: project) }
+ let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1]) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'delegates search to IssuesFinder' do
+ params = { id: list1.id }
+
+ expect_any_instance_of(IssuesFinder).to receive(:execute).once.and_call_original
+
+ described_class.new(project, user, params).execute
+ end
+
+ context 'sets default order to priority' do
+ it 'returns opened issues when listing issues from Backlog' do
+ params = { id: backlog.id }
+
+ issues = described_class.new(project, user, params).execute
+
+ expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1]
+ end
+
+ it 'returns closed issues when listing issues from Done' do
+ params = { id: done.id }
+
+ issues = described_class.new(project, user, params).execute
+
+ expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1]
+ end
+
+ it 'returns opened issues that have label list applied when listing issues from a label list' do
+ params = { id: list1.id }
+
+ issues = described_class.new(project, user, params).execute
+
+ expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2]
+ end
+ end
+ end
+end
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
new file mode 100644
index 00000000000..0122159cab8
--- /dev/null
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+
+describe Boards::Issues::MoveService, services: true do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+
+ let(:bug) { create(:label, project: project, name: 'Bug') }
+ let(:development) { create(:label, project: project, name: 'Development') }
+ let(:testing) { create(:label, project: project, name: 'Testing') }
+
+ let!(:backlog) { create(:backlog_list, board: board) }
+ let!(:list1) { create(:list, board: board, label: development, position: 0) }
+ let!(:list2) { create(:list, board: board, label: testing, position: 1) }
+ let!(:done) { create(:done_list, board: board) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'when moving from backlog' do
+ it 'adds the label of the list it goes to' do
+ issue = create(:labeled_issue, project: project, labels: [bug])
+ params = { from_list_id: backlog.id, to_list_id: list1.id }
+
+ described_class.new(project, user, params).execute(issue)
+
+ expect(issue.reload.labels).to contain_exactly(bug, development)
+ end
+ end
+
+ context 'when moving to backlog' do
+ it 'removes all list-labels' do
+ issue = create(:labeled_issue, project: project, labels: [bug, development, testing])
+ params = { from_list_id: list1.id, to_list_id: backlog.id }
+
+ described_class.new(project, user, params).execute(issue)
+
+ expect(issue.reload.labels).to contain_exactly(bug)
+ end
+ end
+
+ context 'when moving from backlog to done' do
+ it 'closes the issue' do
+ issue = create(:labeled_issue, project: project, labels: [bug])
+ params = { from_list_id: backlog.id, to_list_id: done.id }
+
+ described_class.new(project, user, params).execute(issue)
+ issue.reload
+
+ expect(issue.labels).to contain_exactly(bug)
+ expect(issue).to be_closed
+ end
+ end
+
+ context 'when moving an issue between lists' do
+ let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
+ let(:params) { { from_list_id: list1.id, to_list_id: list2.id } }
+
+ it 'delegates the label changes to Issues::UpdateService' do
+ expect_any_instance_of(Issues::UpdateService).to receive(:execute).with(issue).once
+
+ described_class.new(project, user, params).execute(issue)
+ end
+
+ it 'removes the label from the list it came from and adds the label of the list it goes to' do
+ described_class.new(project, user, params).execute(issue)
+
+ expect(issue.reload.labels).to contain_exactly(bug, testing)
+ end
+ end
+
+ context 'when moving to done' do
+ let(:issue) { create(:labeled_issue, project: project, labels: [bug, development, testing]) }
+ let(:params) { { from_list_id: list2.id, to_list_id: done.id } }
+
+ it 'delegates the close proceedings to Issues::CloseService' do
+ expect_any_instance_of(Issues::CloseService).to receive(:execute).with(issue).once
+
+ described_class.new(project, user, params).execute(issue)
+ end
+
+ it 'removes all list-labels and close the issue' do
+ described_class.new(project, user, params).execute(issue)
+ issue.reload
+
+ expect(issue.labels).to contain_exactly(bug)
+ expect(issue).to be_closed
+ end
+ end
+
+ context 'when moving from done' do
+ let(:issue) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
+ let(:params) { { from_list_id: done.id, to_list_id: list2.id } }
+
+ it 'delegates the re-open proceedings to Issues::ReopenService' do
+ expect_any_instance_of(Issues::ReopenService).to receive(:execute).with(issue).once
+
+ described_class.new(project, user, params).execute(issue)
+ end
+
+ it 'adds the label of the list it goes to and reopen the issue' do
+ described_class.new(project, user, params).execute(issue)
+ issue.reload
+
+ expect(issue.labels).to contain_exactly(bug, testing)
+ expect(issue).to be_reopened
+ end
+ end
+
+ context 'when moving from done to backlog' do
+ it 'reopens the issue' do
+ issue = create(:labeled_issue, :closed, project: project, labels: [bug])
+ params = { from_list_id: done.id, to_list_id: backlog.id }
+
+ described_class.new(project, user, params).execute(issue)
+ issue.reload
+
+ expect(issue.labels).to contain_exactly(bug)
+ expect(issue).to be_reopened
+ end
+ end
+
+ context 'when moving to same list' do
+ let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
+ let(:params) { { from_list_id: list1.id, to_list_id: list1.id } }
+
+ it 'returns false' do
+ expect(described_class.new(project, user, params).execute(issue)).to eq false
+ end
+
+ it 'keeps issues labels' do
+ described_class.new(project, user, params).execute(issue)
+
+ expect(issue.reload.labels).to contain_exactly(bug, development)
+ end
+ end
+ end
+end
diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
new file mode 100644
index 00000000000..5e7e145065e
--- /dev/null
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Boards::Lists::CreateService, services: true do
+ describe '#execute' do
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+ let(:user) { create(:user) }
+ let(:label) { create(:label, name: 'in-progress') }
+
+ subject(:service) { described_class.new(project, user, label_id: label.id) }
+
+ context 'when board lists is empty' do
+ it 'creates a new list at beginning of the list' do
+ list = service.execute
+
+ expect(list.position).to eq 0
+ end
+ end
+
+ context 'when board lists has only a backlog list' do
+ it 'creates a new list at beginning of the list' do
+ create(:backlog_list, board: board)
+
+ list = service.execute
+
+ expect(list.position).to eq 0
+ end
+ end
+
+ context 'when board lists has only labels lists' do
+ it 'creates a new list at end of the lists' do
+ create(:list, board: board, position: 0)
+ create(:list, board: board, position: 1)
+
+ list = service.execute
+
+ expect(list.position).to eq 2
+ end
+ end
+
+ context 'when board lists has backlog, label and done lists' do
+ it 'creates a new list at end of the label lists' do
+ create(:backlog_list, board: board)
+ create(:done_list, board: board)
+ list1 = create(:list, board: board, position: 0)
+
+ list2 = service.execute
+
+ expect(list1.reload.position).to eq 0
+ expect(list2.reload.position).to eq 1
+ end
+ end
+ end
+end
diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb
new file mode 100644
index 00000000000..6eff445feee
--- /dev/null
+++ b/spec/services/boards/lists/destroy_service_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Boards::Lists::DestroyService, services: true do
+ describe '#execute' do
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+ let(:user) { create(:user) }
+
+ context 'when list type is label' do
+ it 'removes list from board' do
+ list = create(:list, board: board)
+ service = described_class.new(project, user)
+
+ expect { service.execute(list) }.to change(board.lists, :count).by(-1)
+ end
+
+ it 'decrements position of higher lists' do
+ backlog = create(:backlog_list, board: board)
+ development = create(:list, board: board, position: 0)
+ review = create(:list, board: board, position: 1)
+ staging = create(:list, board: board, position: 2)
+ done = create(:done_list, board: board)
+
+ described_class.new(project, user).execute(development)
+
+ expect(backlog.reload.position).to be_nil
+ expect(review.reload.position).to eq 0
+ expect(staging.reload.position).to eq 1
+ expect(done.reload.position).to be_nil
+ end
+ end
+
+ it 'does not remove list from board when list type is backlog' do
+ list = create(:backlog_list, board: board)
+ service = described_class.new(project, user)
+
+ expect { service.execute(list) }.not_to change(board.lists, :count)
+ end
+
+ it 'does not remove list from board when list type is done' do
+ list = create(:done_list, board: board)
+ service = described_class.new(project, user)
+
+ expect { service.execute(list) }.not_to change(board.lists, :count)
+ end
+ end
+end
diff --git a/spec/services/boards/lists/generate_service_spec.rb b/spec/services/boards/lists/generate_service_spec.rb
new file mode 100644
index 00000000000..9fd39122737
--- /dev/null
+++ b/spec/services/boards/lists/generate_service_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Boards::Lists::GenerateService, services: true do
+ describe '#execute' do
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+ let(:user) { create(:user) }
+
+ subject(:service) { described_class.new(project, user) }
+
+ context 'when board lists is empty' do
+ it 'creates the default lists' do
+ expect { service.execute }.to change(board.lists, :count).by(4)
+ end
+ end
+
+ context 'when board lists is not empty' do
+ it 'does not creates the default lists' do
+ create(:list, board: board)
+
+ expect { service.execute }.not_to change(board.lists, :count)
+ end
+ end
+
+ context 'when project labels does not contains any list label' do
+ it 'creates labels' do
+ expect { service.execute }.to change(project.labels, :count).by(4)
+ end
+ end
+
+ context 'when project labels contains some of list label' do
+ it 'creates the missing labels' do
+ create(:label, project: project, name: 'Development')
+ create(:label, project: project, name: 'Ready')
+
+ expect { service.execute }.to change(project.labels, :count).by(2)
+ end
+ end
+ end
+end
diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb
new file mode 100644
index 00000000000..3e9b7d07fc6
--- /dev/null
+++ b/spec/services/boards/lists/move_service_spec.rb
@@ -0,0 +1,110 @@
+require 'spec_helper'
+
+describe Boards::Lists::MoveService, services: true do
+ describe '#execute' do
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+ let(:user) { create(:user) }
+
+ let!(:backlog) { create(:backlog_list, board: board) }
+ let!(:planning) { create(:list, board: board, position: 0) }
+ let!(:development) { create(:list, board: board, position: 1) }
+ let!(:review) { create(:list, board: board, position: 2) }
+ let!(:staging) { create(:list, board: board, position: 3) }
+ let!(:done) { create(:done_list, board: board) }
+
+ context 'when list type is set to label' do
+ it 'keeps position of lists when new position is nil' do
+ service = described_class.new(project, user, position: nil)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+
+ it 'keeps position of lists when new positon is equal to old position' do
+ service = described_class.new(project, user, position: planning.position)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+
+ it 'keeps position of lists when new positon is negative' do
+ service = described_class.new(project, user, position: -1)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+
+ it 'keeps position of lists when new positon is equal to number of labels lists' do
+ service = described_class.new(project, user, position: board.lists.label.size)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+
+ it 'keeps position of lists when new positon is greater than number of labels lists' do
+ service = described_class.new(project, user, position: board.lists.label.size + 1)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+
+ it 'increments position of intermediate lists when new positon is equal to first position' do
+ service = described_class.new(project, user, position: 0)
+
+ service.execute(staging)
+
+ expect(current_list_positions).to eq [1, 2, 3, 0]
+ end
+
+ it 'decrements position of intermediate lists when new positon is equal to last position' do
+ service = described_class.new(project, user, position: board.lists.label.last.position)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [3, 0, 1, 2]
+ end
+
+ it 'decrements position of intermediate lists when new position is greater than old position' do
+ service = described_class.new(project, user, position: 2)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [2, 0, 1, 3]
+ end
+
+ it 'increments position of intermediate lists when new position is lower than old position' do
+ service = described_class.new(project, user, position: 1)
+
+ service.execute(staging)
+
+ expect(current_list_positions).to eq [0, 2, 3, 1]
+ end
+ end
+
+ it 'keeps position of lists when list type is backlog' do
+ service = described_class.new(project, user, position: 2)
+
+ service.execute(backlog)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+
+ it 'keeps position of lists when list type is done' do
+ service = described_class.new(project, user, position: 2)
+
+ service.execute(done)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+ end
+
+ def current_list_positions
+ [planning, development, review, staging].map { |list| list.reload.position }
+ end
+end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 088c3d48bf7..0313f424463 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -319,5 +319,10 @@ describe Issues::UpdateService, services: true do
end
end
end
+
+ context 'updating mentions' do
+ let(:mentionable) { issue }
+ include_examples 'updating mentions', Issues::UpdateService
+ end
end
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 283a336afd9..6dfeb581975 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -226,6 +226,11 @@ describe MergeRequests::UpdateService, services: true do
end
end
+ context 'updating mentions' do
+ let(:mentionable) { merge_request }
+ include_examples 'updating mentions', MergeRequests::UpdateService
+ end
+
context 'when MergeRequest has tasks' do
before { update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) }
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 92b441c28ca..62c97e09288 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -9,6 +9,28 @@ describe NotificationService, services: true do
end
end
+ shared_examples 'notifications for new mentions' do
+ def send_notifications(*new_mentions)
+ reset_delivered_emails!
+ notification.send(notification_method, mentionable, new_mentions, @u_disabled)
+ end
+
+ it 'sends no emails when no new mentions are present' do
+ send_notifications
+ expect(ActionMailer::Base.deliveries).to be_empty
+ end
+
+ it 'emails new mentions with a watch level higher than participant' do
+ send_notifications(@u_watcher, @u_participant_mentioned, @u_custom_global)
+ should_only_email(@u_watcher, @u_participant_mentioned, @u_custom_global)
+ end
+
+ it 'does not email new mentions with a watch level equal to or less than participant' do
+ send_notifications(@u_participating, @u_mentioned)
+ expect(ActionMailer::Base.deliveries).to be_empty
+ end
+ end
+
describe 'Keys' do
describe '#new_key' do
let!(:key) { create(:personal_key) }
@@ -399,6 +421,13 @@ describe NotificationService, services: true do
end
end
+ describe '#new_mentions_in_issue' do
+ let(:notification_method) { :new_mentions_in_issue }
+ let(:mentionable) { issue }
+
+ include_examples 'notifications for new mentions'
+ end
+
describe '#reassigned_issue' do
before do
update_custom_notification(:reassign_issue, @u_guest_custom, project)
@@ -700,6 +729,8 @@ describe NotificationService, services: true do
before do
build_team(merge_request.target_project)
add_users_with_subscription(merge_request.target_project, merge_request)
+ update_custom_notification(:new_merge_request, @u_guest_custom, project)
+ update_custom_notification(:new_merge_request, @u_custom_global)
ActionMailer::Base.deliveries.clear
end
@@ -763,6 +794,13 @@ describe NotificationService, services: true do
end
end
+ describe '#new_mentions_in_merge_request' do
+ let(:notification_method) { :new_mentions_in_merge_request }
+ let(:mentionable) { merge_request }
+
+ include_examples 'notifications for new mentions'
+ end
+
describe '#reassigned_merge_request' do
before do
update_custom_notification(:reassign_merge_request, @u_guest_custom, project)
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 6c3cbeae13c..a2a1d5e6d30 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -194,12 +194,12 @@ describe TodoService, services: true do
end
end
- describe '#mark_todos_as_done' do
- it 'marks related todos for the user as done' do
- first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
- second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
+ shared_examples 'marking todos as done' do |meth|
+ let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
+ let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
- service.mark_todos_as_done([first_todo, second_todo], john_doe)
+ it 'marks related todos for the user as done' do
+ service.send(meth, collection, john_doe)
expect(first_todo.reload).to be_done
expect(second_todo.reload).to be_done
@@ -207,20 +207,30 @@ describe TodoService, services: true do
describe 'cached counts' do
it 'updates when todos change' do
- todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
-
expect(john_doe.todos_done_count).to eq(0)
- expect(john_doe.todos_pending_count).to eq(1)
+ expect(john_doe.todos_pending_count).to eq(2)
expect(john_doe).to receive(:update_todos_count_cache).and_call_original
- service.mark_todos_as_done([todo], john_doe)
+ service.send(meth, collection, john_doe)
- expect(john_doe.todos_done_count).to eq(1)
+ expect(john_doe.todos_done_count).to eq(2)
expect(john_doe.todos_pending_count).to eq(0)
end
end
end
+ describe '#mark_todos_as_done' do
+ it_behaves_like 'marking todos as done', :mark_todos_as_done do
+ let(:collection) { [first_todo, second_todo] }
+ end
+ end
+
+ describe '#mark_todos_as_done_by_ids' do
+ it_behaves_like 'marking todos as done', :mark_todos_as_done_by_ids do
+ let(:collection) { [first_todo, second_todo].map(&:id) }
+ end
+ end
+
describe '#new_note' do
let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
diff --git a/spec/support/api/schema_matcher.rb b/spec/support/api/schema_matcher.rb
new file mode 100644
index 00000000000..e42d727672b
--- /dev/null
+++ b/spec/support/api/schema_matcher.rb
@@ -0,0 +1,8 @@
+RSpec::Matchers.define :match_response_schema do |schema, **options|
+ match do |response|
+ schema_directory = "#{Dir.pwd}/spec/fixtures/api/schemas"
+ schema_path = "#{schema_directory}/#{schema}.json"
+
+ JSON::Validator.validate!(schema_path, response.body, options)
+ end
+end
diff --git a/spec/support/email_helpers.rb b/spec/support/email_helpers.rb
index a85ab22ce36..0bfc4685532 100644
--- a/spec/support/email_helpers.rb
+++ b/spec/support/email_helpers.rb
@@ -3,6 +3,16 @@ module EmailHelpers
ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1
end
+ def reset_delivered_emails!
+ ActionMailer::Base.deliveries.clear
+ end
+
+ def should_only_email(*users)
+ users.each {|user| should_email(user) }
+ recipients = ActionMailer::Base.deliveries.flat_map(&:to)
+ expect(recipients.count).to eq(users.count)
+ end
+
def should_email(user)
expect(sent_to_user?(user)).to be_truthy
end
diff --git a/spec/support/fake_u2f_device.rb b/spec/support/fake_u2f_device.rb
index f550e9a0160..8c407b867fe 100644
--- a/spec/support/fake_u2f_device.rb
+++ b/spec/support/fake_u2f_device.rb
@@ -1,6 +1,9 @@
class FakeU2fDevice
- def initialize(page)
+ attr_reader :name
+
+ def initialize(page, name)
@page = page
+ @name = name
end
def respond_to_u2f_registration
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 1c0c66969e3..edbbfc3c9e5 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -5,25 +5,31 @@ module TestEnv
# When developing the seed repository, comment out the branch you will modify.
BRANCH_SHA = {
- 'empty-branch' => '7efb185',
- 'ends-with.json' => '98b0d8b3',
- 'flatten-dir' => 'e56497b',
- 'feature' => '0b4bc9a',
- 'feature_conflict' => 'bb5206f',
- 'fix' => '48f0be4',
- 'improve/awesome' => '5937ac0',
- 'markdown' => '0ed8c6c',
- 'lfs' => 'be93687',
- 'master' => '5937ac0',
- "'test'" => 'e56497b',
- 'orphaned-branch' => '45127a9',
- 'binary-encoding' => '7b1cf43',
- 'gitattributes' => '5a62481',
- 'expand-collapse-diffs' => '4842455',
- 'expand-collapse-files' => '025db92',
- 'expand-collapse-lines' => '238e82d',
- 'video' => '8879059',
- 'crlf-diff' => '5938907'
+ 'empty-branch' => '7efb185',
+ 'ends-with.json' => '98b0d8b3',
+ 'flatten-dir' => 'e56497b',
+ 'feature' => '0b4bc9a',
+ 'feature_conflict' => 'bb5206f',
+ 'fix' => '48f0be4',
+ 'improve/awesome' => '5937ac0',
+ 'markdown' => '0ed8c6c',
+ 'lfs' => 'be93687',
+ 'master' => '5937ac0',
+ "'test'" => 'e56497b',
+ 'orphaned-branch' => '45127a9',
+ 'binary-encoding' => '7b1cf43',
+ 'gitattributes' => '5a62481',
+ 'expand-collapse-diffs' => '4842455',
+ 'expand-collapse-files' => '025db92',
+ 'expand-collapse-lines' => '238e82d',
+ 'video' => '8879059',
+ 'crlf-diff' => '5938907',
+ 'conflict-start' => '14fa46b',
+ 'conflict-resolvable' => '1450cd6',
+ 'conflict-binary-file' => '259a6fb',
+ 'conflict-contains-conflict-markers' => '5e0964c',
+ 'conflict-missing-side' => 'eb227b3',
+ 'conflict-too-large' => '39fa04f',
}
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
diff --git a/spec/support/updating_mentions_shared_examples.rb b/spec/support/updating_mentions_shared_examples.rb
new file mode 100644
index 00000000000..e0c59a5c280
--- /dev/null
+++ b/spec/support/updating_mentions_shared_examples.rb
@@ -0,0 +1,32 @@
+RSpec.shared_examples 'updating mentions' do |service_class|
+ let(:mentioned_user) { create(:user) }
+ let(:service_class) { service_class }
+
+ before { project.team << [mentioned_user, :developer] }
+
+ def update_mentionable(opts)
+ reset_delivered_emails!
+
+ perform_enqueued_jobs do
+ service_class.new(project, user, opts).execute(mentionable)
+ end
+
+ mentionable.reload
+ end
+
+ context 'in title' do
+ before { update_mentionable(title: mentioned_user.to_reference) }
+
+ it 'emails only the newly-mentioned user' do
+ should_only_email(mentioned_user)
+ end
+ end
+
+ context 'in description' do
+ before { update_mentionable(description: mentioned_user.to_reference) }
+
+ it 'emails only the newly-mentioned user' do
+ should_only_email(mentioned_user)
+ end
+ end
+end
diff --git a/vendor/assets/javascripts/Sortable.js b/vendor/assets/javascripts/Sortable.js
new file mode 100644
index 00000000000..eca7c5012b2
--- /dev/null
+++ b/vendor/assets/javascripts/Sortable.js
@@ -0,0 +1,1285 @@
+/**!
+ * Sortable
+ * @author RubaXa <trash@rubaxa.org>
+ * @license MIT
+ */
+
+
+(function (factory) {
+ "use strict";
+
+ if (typeof define === "function" && define.amd) {
+ define(factory);
+ }
+ else if (typeof module != "undefined" && typeof module.exports != "undefined") {
+ module.exports = factory();
+ }
+ else if (typeof Package !== "undefined") {
+ Sortable = factory(); // export for Meteor.js
+ }
+ else {
+ /* jshint sub:true */
+ window["Sortable"] = factory();
+ }
+})(function () {
+ "use strict";
+
+ var dragEl,
+ parentEl,
+ ghostEl,
+ cloneEl,
+ rootEl,
+ nextEl,
+
+ scrollEl,
+ scrollParentEl,
+
+ lastEl,
+ lastCSS,
+ lastParentCSS,
+
+ oldIndex,
+ newIndex,
+
+ activeGroup,
+ autoScroll = {},
+
+ tapEvt,
+ touchEvt,
+
+ moved,
+
+ /** @const */
+ RSPACE = /\s+/g,
+
+ expando = 'Sortable' + (new Date).getTime(),
+
+ win = window,
+ document = win.document,
+ parseInt = win.parseInt,
+
+ supportDraggable = !!('draggable' in document.createElement('div')),
+ supportCssPointerEvents = (function (el) {
+ el = document.createElement('x');
+ el.style.cssText = 'pointer-events:auto';
+ return el.style.pointerEvents === 'auto';
+ })(),
+
+ _silent = false,
+
+ abs = Math.abs,
+ min = Math.min,
+ slice = [].slice,
+
+ touchDragOverListeners = [],
+
+ _autoScroll = _throttle(function (/**Event*/evt, /**Object*/options, /**HTMLElement*/rootEl) {
+ // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521
+ if (rootEl && options.scroll) {
+ var el,
+ rect,
+ sens = options.scrollSensitivity,
+ speed = options.scrollSpeed,
+
+ x = evt.clientX,
+ y = evt.clientY,
+
+ winWidth = window.innerWidth,
+ winHeight = window.innerHeight,
+
+ vx,
+ vy
+ ;
+
+ // Delect scrollEl
+ if (scrollParentEl !== rootEl) {
+ scrollEl = options.scroll;
+ scrollParentEl = rootEl;
+
+ if (scrollEl === true) {
+ scrollEl = rootEl;
+
+ do {
+ if ((scrollEl.offsetWidth < scrollEl.scrollWidth) ||
+ (scrollEl.offsetHeight < scrollEl.scrollHeight)
+ ) {
+ break;
+ }
+ /* jshint boss:true */
+ } while (scrollEl = scrollEl.parentNode);
+ }
+ }
+
+ if (scrollEl) {
+ el = scrollEl;
+ rect = scrollEl.getBoundingClientRect();
+ vx = (abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens);
+ vy = (abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens);
+ }
+
+
+ if (!(vx || vy)) {
+ vx = (winWidth - x <= sens) - (x <= sens);
+ vy = (winHeight - y <= sens) - (y <= sens);
+
+ /* jshint expr:true */
+ (vx || vy) && (el = win);
+ }
+
+
+ if (autoScroll.vx !== vx || autoScroll.vy !== vy || autoScroll.el !== el) {
+ autoScroll.el = el;
+ autoScroll.vx = vx;
+ autoScroll.vy = vy;
+
+ clearInterval(autoScroll.pid);
+
+ if (el) {
+ autoScroll.pid = setInterval(function () {
+ if (el === win) {
+ win.scrollTo(win.pageXOffset + vx * speed, win.pageYOffset + vy * speed);
+ } else {
+ vy && (el.scrollTop += vy * speed);
+ vx && (el.scrollLeft += vx * speed);
+ }
+ }, 24);
+ }
+ }
+ }
+ }, 30),
+
+ _prepareGroup = function (options) {
+ var group = options.group;
+
+ if (!group || typeof group != 'object') {
+ group = options.group = {name: group};
+ }
+
+ ['pull', 'put'].forEach(function (key) {
+ if (!(key in group)) {
+ group[key] = true;
+ }
+ });
+
+ options.groups = ' ' + group.name + (group.put.join ? ' ' + group.put.join(' ') : '') + ' ';
+ }
+ ;
+
+
+
+ /**
+ * @class Sortable
+ * @param {HTMLElement} el
+ * @param {Object} [options]
+ */
+ function Sortable(el, options) {
+ if (!(el && el.nodeType && el.nodeType === 1)) {
+ throw 'Sortable: `el` must be HTMLElement, and not ' + {}.toString.call(el);
+ }
+
+ this.el = el; // root element
+ this.options = options = _extend({}, options);
+
+
+ // Export instance
+ el[expando] = this;
+
+
+ // Default options
+ var defaults = {
+ group: Math.random(),
+ sort: true,
+ disabled: false,
+ store: null,
+ handle: null,
+ scroll: true,
+ scrollSensitivity: 30,
+ scrollSpeed: 10,
+ draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*',
+ ghostClass: 'sortable-ghost',
+ chosenClass: 'sortable-chosen',
+ ignore: 'a, img',
+ filter: null,
+ animation: 0,
+ setData: function (dataTransfer, dragEl) {
+ dataTransfer.setData('Text', dragEl.textContent);
+ },
+ dropBubble: false,
+ dragoverBubble: false,
+ dataIdAttr: 'data-id',
+ delay: 0,
+ forceFallback: false,
+ fallbackClass: 'sortable-fallback',
+ fallbackOnBody: false,
+ fallbackTolerance: 0
+ };
+
+
+ // Set default options
+ for (var name in defaults) {
+ !(name in options) && (options[name] = defaults[name]);
+ }
+
+ _prepareGroup(options);
+
+ // Bind all private methods
+ for (var fn in this) {
+ if (fn.charAt(0) === '_') {
+ this[fn] = this[fn].bind(this);
+ }
+ }
+
+ // Setup drag mode
+ this.nativeDraggable = options.forceFallback ? false : supportDraggable;
+
+ // Bind events
+ _on(el, 'mousedown', this._onTapStart);
+ _on(el, 'touchstart', this._onTapStart);
+
+ if (this.nativeDraggable) {
+ _on(el, 'dragover', this);
+ _on(el, 'dragenter', this);
+ }
+
+ touchDragOverListeners.push(this._onDragOver);
+
+ // Restore sorting
+ options.store && this.sort(options.store.get(this));
+ }
+
+
+ Sortable.prototype = /** @lends Sortable.prototype */ {
+ constructor: Sortable,
+
+ _onTapStart: function (/** Event|TouchEvent */evt) {
+ var _this = this,
+ el = this.el,
+ options = this.options,
+ type = evt.type,
+ touch = evt.touches && evt.touches[0],
+ target = (touch || evt).target,
+ originalTarget = target,
+ filter = options.filter,
+ startIndex;
+
+ // Don't trigger start event when an element is been dragged, otherwise the evt.oldindex always wrong when set option.group.
+ if (dragEl) {
+ return;
+ }
+
+ if (type === 'mousedown' && evt.button !== 0 || options.disabled) {
+ return; // only left button or enabled
+ }
+
+ target = _closest(target, options.draggable, el);
+
+ if (!target) {
+ return;
+ }
+
+ if (options.handle && !_closest(originalTarget, options.handle, el)) {
+ return;
+ }
+
+ // Get the index of the dragged element within its parent
+ startIndex = _index(target, options.draggable);
+
+ // Check filter
+ if (typeof filter === 'function') {
+ if (filter.call(this, evt, target, this)) {
+ _dispatchEvent(_this, originalTarget, 'filter', target, el, startIndex);
+ evt.preventDefault();
+ return; // cancel dnd
+ }
+ }
+ else if (filter) {
+ filter = filter.split(',').some(function (criteria) {
+ criteria = _closest(originalTarget, criteria.trim(), el);
+
+ if (criteria) {
+ _dispatchEvent(_this, criteria, 'filter', target, el, startIndex);
+ return true;
+ }
+ });
+
+ if (filter) {
+ evt.preventDefault();
+ return; // cancel dnd
+ }
+ }
+
+ // Prepare `dragstart`
+ this._prepareDragStart(evt, touch, target, startIndex);
+ },
+
+ _prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target, /** Number */startIndex) {
+ var _this = this,
+ el = _this.el,
+ options = _this.options,
+ ownerDocument = el.ownerDocument,
+ dragStartFn;
+
+ if (target && !dragEl && (target.parentNode === el)) {
+ tapEvt = evt;
+
+ rootEl = el;
+ dragEl = target;
+ parentEl = dragEl.parentNode;
+ nextEl = dragEl.nextSibling;
+ activeGroup = options.group;
+ oldIndex = startIndex;
+
+ this._lastX = (touch || evt).clientX;
+ this._lastY = (touch || evt).clientY;
+
+ dragStartFn = function () {
+ // Delayed drag has been triggered
+ // we can re-enable the events: touchmove/mousemove
+ _this._disableDelayedDrag();
+
+ // Make the element draggable
+ dragEl.draggable = true;
+
+ // Chosen item
+ _toggleClass(dragEl, _this.options.chosenClass, true);
+
+ // Bind the events: dragstart/dragend
+ _this._triggerDragStart(touch);
+
+ // Drag start event
+ _dispatchEvent(_this, rootEl, 'choose', dragEl, rootEl, oldIndex);
+ };
+
+ // Disable "draggable"
+ options.ignore.split(',').forEach(function (criteria) {
+ _find(dragEl, criteria.trim(), _disableDraggable);
+ });
+
+ _on(ownerDocument, 'mouseup', _this._onDrop);
+ _on(ownerDocument, 'touchend', _this._onDrop);
+ _on(ownerDocument, 'touchcancel', _this._onDrop);
+
+ if (options.delay) {
+ // If the user moves the pointer or let go the click or touch
+ // before the delay has been reached:
+ // disable the delayed drag
+ _on(ownerDocument, 'mouseup', _this._disableDelayedDrag);
+ _on(ownerDocument, 'touchend', _this._disableDelayedDrag);
+ _on(ownerDocument, 'touchcancel', _this._disableDelayedDrag);
+ _on(ownerDocument, 'mousemove', _this._disableDelayedDrag);
+ _on(ownerDocument, 'touchmove', _this._disableDelayedDrag);
+
+ _this._dragStartTimer = setTimeout(dragStartFn, options.delay);
+ } else {
+ dragStartFn();
+ }
+ }
+ },
+
+ _disableDelayedDrag: function () {
+ var ownerDocument = this.el.ownerDocument;
+
+ clearTimeout(this._dragStartTimer);
+ _off(ownerDocument, 'mouseup', this._disableDelayedDrag);
+ _off(ownerDocument, 'touchend', this._disableDelayedDrag);
+ _off(ownerDocument, 'touchcancel', this._disableDelayedDrag);
+ _off(ownerDocument, 'mousemove', this._disableDelayedDrag);
+ _off(ownerDocument, 'touchmove', this._disableDelayedDrag);
+ },
+
+ _triggerDragStart: function (/** Touch */touch) {
+ if (touch) {
+ // Touch device support
+ tapEvt = {
+ target: dragEl,
+ clientX: touch.clientX,
+ clientY: touch.clientY
+ };
+
+ this._onDragStart(tapEvt, 'touch');
+ }
+ else if (!this.nativeDraggable) {
+ this._onDragStart(tapEvt, true);
+ }
+ else {
+ _on(dragEl, 'dragend', this);
+ _on(rootEl, 'dragstart', this._onDragStart);
+ }
+
+ try {
+ if (document.selection) {
+ document.selection.empty();
+ } else {
+ window.getSelection().removeAllRanges();
+ }
+ } catch (err) {
+ }
+ },
+
+ _dragStarted: function () {
+ if (rootEl && dragEl) {
+ // Apply effect
+ _toggleClass(dragEl, this.options.ghostClass, true);
+
+ Sortable.active = this;
+
+ // Drag start event
+ _dispatchEvent(this, rootEl, 'start', dragEl, rootEl, oldIndex);
+ }
+ },
+
+ _emulateDragOver: function () {
+ if (touchEvt) {
+ if (this._lastX === touchEvt.clientX && this._lastY === touchEvt.clientY) {
+ return;
+ }
+
+ this._lastX = touchEvt.clientX;
+ this._lastY = touchEvt.clientY;
+
+ if (!supportCssPointerEvents) {
+ _css(ghostEl, 'display', 'none');
+ }
+
+ var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY),
+ parent = target,
+ groupName = ' ' + this.options.group.name + '',
+ i = touchDragOverListeners.length;
+
+ if (parent) {
+ do {
+ if (parent[expando] && parent[expando].options.groups.indexOf(groupName) > -1) {
+ while (i--) {
+ touchDragOverListeners[i]({
+ clientX: touchEvt.clientX,
+ clientY: touchEvt.clientY,
+ target: target,
+ rootEl: parent
+ });
+ }
+
+ break;
+ }
+
+ target = parent; // store last element
+ }
+ /* jshint boss:true */
+ while (parent = parent.parentNode);
+ }
+
+ if (!supportCssPointerEvents) {
+ _css(ghostEl, 'display', '');
+ }
+ }
+ },
+
+
+ _onTouchMove: function (/**TouchEvent*/evt) {
+ if (tapEvt) {
+ var options = this.options,
+ fallbackTolerance = options.fallbackTolerance,
+ touch = evt.touches ? evt.touches[0] : evt,
+ dx = touch.clientX - tapEvt.clientX,
+ dy = touch.clientY - tapEvt.clientY,
+ translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)';
+
+ // only set the status to dragging, when we are actually dragging
+ if (!Sortable.active) {
+ if (fallbackTolerance &&
+ min(abs(touch.clientX - this._lastX), abs(touch.clientY - this._lastY)) < fallbackTolerance
+ ) {
+ return;
+ }
+
+ this._dragStarted();
+ }
+
+ // as well as creating the ghost element on the document body
+ this._appendGhost();
+
+ moved = true;
+ touchEvt = touch;
+
+ _css(ghostEl, 'webkitTransform', translate3d);
+ _css(ghostEl, 'mozTransform', translate3d);
+ _css(ghostEl, 'msTransform', translate3d);
+ _css(ghostEl, 'transform', translate3d);
+
+ evt.preventDefault();
+ }
+ },
+
+ _appendGhost: function () {
+ if (!ghostEl) {
+ var rect = dragEl.getBoundingClientRect(),
+ css = _css(dragEl),
+ options = this.options,
+ ghostRect;
+
+ ghostEl = dragEl.cloneNode(true);
+
+ _toggleClass(ghostEl, options.ghostClass, false);
+ _toggleClass(ghostEl, options.fallbackClass, true);
+
+ _css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10));
+ _css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10));
+ _css(ghostEl, 'width', rect.width);
+ _css(ghostEl, 'height', rect.height);
+ _css(ghostEl, 'opacity', '0.8');
+ _css(ghostEl, 'position', 'fixed');
+ _css(ghostEl, 'zIndex', '100000');
+ _css(ghostEl, 'pointerEvents', 'none');
+
+ options.fallbackOnBody && document.body.appendChild(ghostEl) || rootEl.appendChild(ghostEl);
+
+ // Fixing dimensions.
+ ghostRect = ghostEl.getBoundingClientRect();
+ _css(ghostEl, 'width', rect.width * 2 - ghostRect.width);
+ _css(ghostEl, 'height', rect.height * 2 - ghostRect.height);
+ }
+ },
+
+ _onDragStart: function (/**Event*/evt, /**boolean*/useFallback) {
+ var dataTransfer = evt.dataTransfer,
+ options = this.options;
+
+ this._offUpEvents();
+
+ if (activeGroup.pull == 'clone') {
+ cloneEl = dragEl.cloneNode(true);
+ _css(cloneEl, 'display', 'none');
+ rootEl.insertBefore(cloneEl, dragEl);
+ _dispatchEvent(this, rootEl, 'clone', dragEl);
+ }
+
+ if (useFallback) {
+ if (useFallback === 'touch') {
+ // Bind touch events
+ _on(document, 'touchmove', this._onTouchMove);
+ _on(document, 'touchend', this._onDrop);
+ _on(document, 'touchcancel', this._onDrop);
+ } else {
+ // Old brwoser
+ _on(document, 'mousemove', this._onTouchMove);
+ _on(document, 'mouseup', this._onDrop);
+ }
+
+ this._loopId = setInterval(this._emulateDragOver, 50);
+ }
+ else {
+ if (dataTransfer) {
+ dataTransfer.effectAllowed = 'move';
+ options.setData && options.setData.call(this, dataTransfer, dragEl);
+ }
+
+ _on(document, 'drop', this);
+ setTimeout(this._dragStarted, 0);
+ }
+ },
+
+ _onDragOver: function (/**Event*/evt) {
+ var el = this.el,
+ target,
+ dragRect,
+ revert,
+ options = this.options,
+ group = options.group,
+ groupPut = group.put,
+ isOwner = (activeGroup === group),
+ canSort = options.sort;
+
+ if (evt.preventDefault !== void 0) {
+ evt.preventDefault();
+ !options.dragoverBubble && evt.stopPropagation();
+ }
+
+ moved = true;
+
+ if (activeGroup && !options.disabled &&
+ (isOwner
+ ? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list
+ : activeGroup.pull && groupPut && (
+ (activeGroup.name === group.name) || // by Name
+ (groupPut.indexOf && ~groupPut.indexOf(activeGroup.name)) // by Array
+ )
+ ) &&
+ (evt.rootEl === void 0 || evt.rootEl === this.el) // touch fallback
+ ) {
+ // Smart auto-scrolling
+ _autoScroll(evt, options, this.el);
+
+ if (_silent) {
+ return;
+ }
+
+ target = _closest(evt.target, options.draggable, el);
+ dragRect = dragEl.getBoundingClientRect();
+
+ if (revert) {
+ _cloneHide(true);
+ parentEl = rootEl; // actualization
+
+ if (cloneEl || nextEl) {
+ rootEl.insertBefore(dragEl, cloneEl || nextEl);
+ }
+ else if (!canSort) {
+ rootEl.appendChild(dragEl);
+ }
+
+ return;
+ }
+
+
+ if ((el.children.length === 0) || (el.children[0] === ghostEl) ||
+ (el === evt.target) && (target = _ghostIsLast(el, evt))
+ ) {
+
+ if (target) {
+ if (target.animated) {
+ return;
+ }
+
+ targetRect = target.getBoundingClientRect();
+ }
+
+ _cloneHide(isOwner);
+
+ if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect) !== false) {
+ if (!dragEl.contains(el)) {
+ el.appendChild(dragEl);
+ parentEl = el; // actualization
+ }
+
+ this._animate(dragRect, dragEl);
+ target && this._animate(targetRect, target);
+ }
+ }
+ else if (target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0)) {
+ if (lastEl !== target) {
+ lastEl = target;
+ lastCSS = _css(target);
+ lastParentCSS = _css(target.parentNode);
+ }
+
+
+ var targetRect = target.getBoundingClientRect(),
+ width = targetRect.right - targetRect.left,
+ height = targetRect.bottom - targetRect.top,
+ floating = /left|right|inline/.test(lastCSS.cssFloat + lastCSS.display)
+ || (lastParentCSS.display == 'flex' && lastParentCSS['flex-direction'].indexOf('row') === 0),
+ isWide = (target.offsetWidth > dragEl.offsetWidth),
+ isLong = (target.offsetHeight > dragEl.offsetHeight),
+ halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5,
+ nextSibling = target.nextElementSibling,
+ moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect),
+ after
+ ;
+
+ if (moveVector !== false) {
+ _silent = true;
+ setTimeout(_unsilent, 30);
+
+ _cloneHide(isOwner);
+
+ if (moveVector === 1 || moveVector === -1) {
+ after = (moveVector === 1);
+ }
+ else if (floating) {
+ var elTop = dragEl.offsetTop,
+ tgTop = target.offsetTop;
+
+ if (elTop === tgTop) {
+ after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide;
+ }
+ else if (target.previousElementSibling === dragEl || dragEl.previousElementSibling === target) {
+ after = (evt.clientY - targetRect.top) / height > 0.5;
+ } else {
+ after = tgTop > elTop;
+ }
+ } else {
+ after = (nextSibling !== dragEl) && !isLong || halfway && isLong;
+ }
+
+ if (!dragEl.contains(el)) {
+ if (after && !nextSibling) {
+ el.appendChild(dragEl);
+ } else {
+ target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
+ }
+ }
+
+ parentEl = dragEl.parentNode; // actualization
+
+ this._animate(dragRect, dragEl);
+ this._animate(targetRect, target);
+ }
+ }
+ }
+ },
+
+ _animate: function (prevRect, target) {
+ var ms = this.options.animation;
+
+ if (ms) {
+ var currentRect = target.getBoundingClientRect();
+
+ _css(target, 'transition', 'none');
+ _css(target, 'transform', 'translate3d('
+ + (prevRect.left - currentRect.left) + 'px,'
+ + (prevRect.top - currentRect.top) + 'px,0)'
+ );
+
+ target.offsetWidth; // repaint
+
+ _css(target, 'transition', 'all ' + ms + 'ms');
+ _css(target, 'transform', 'translate3d(0,0,0)');
+
+ clearTimeout(target.animated);
+ target.animated = setTimeout(function () {
+ _css(target, 'transition', '');
+ _css(target, 'transform', '');
+ target.animated = false;
+ }, ms);
+ }
+ },
+
+ _offUpEvents: function () {
+ var ownerDocument = this.el.ownerDocument;
+
+ _off(document, 'touchmove', this._onTouchMove);
+ _off(ownerDocument, 'mouseup', this._onDrop);
+ _off(ownerDocument, 'touchend', this._onDrop);
+ _off(ownerDocument, 'touchcancel', this._onDrop);
+ },
+
+ _onDrop: function (/**Event*/evt) {
+ var el = this.el,
+ options = this.options;
+
+ clearInterval(this._loopId);
+ clearInterval(autoScroll.pid);
+ clearTimeout(this._dragStartTimer);
+
+ // Unbind events
+ _off(document, 'mousemove', this._onTouchMove);
+
+ if (this.nativeDraggable) {
+ _off(document, 'drop', this);
+ _off(el, 'dragstart', this._onDragStart);
+ }
+
+ this._offUpEvents();
+
+ if (evt) {
+ if (moved) {
+ evt.preventDefault();
+ !options.dropBubble && evt.stopPropagation();
+ }
+
+ ghostEl && ghostEl.parentNode.removeChild(ghostEl);
+
+ if (dragEl) {
+ if (this.nativeDraggable) {
+ _off(dragEl, 'dragend', this);
+ }
+
+ _disableDraggable(dragEl);
+
+ // Remove class's
+ _toggleClass(dragEl, this.options.ghostClass, false);
+ _toggleClass(dragEl, this.options.chosenClass, false);
+
+ if (rootEl !== parentEl) {
+ newIndex = _index(dragEl, options.draggable);
+
+ if (newIndex >= 0) {
+ // drag from one list and drop into another
+ _dispatchEvent(null, parentEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
+ _dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
+
+ // Add event
+ _dispatchEvent(null, parentEl, 'add', dragEl, rootEl, oldIndex, newIndex);
+
+ // Remove event
+ _dispatchEvent(this, rootEl, 'remove', dragEl, rootEl, oldIndex, newIndex);
+ }
+ }
+ else {
+ // Remove clone
+ cloneEl && cloneEl.parentNode.removeChild(cloneEl);
+
+ if (dragEl.nextSibling !== nextEl) {
+ // Get the index of the dragged element within its parent
+ newIndex = _index(dragEl, options.draggable);
+
+ if (newIndex >= 0) {
+ // drag & drop within the same list
+ _dispatchEvent(this, rootEl, 'update', dragEl, rootEl, oldIndex, newIndex);
+ _dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
+ }
+ }
+ }
+
+ if (Sortable.active) {
+ if (newIndex === null || newIndex === -1) {
+ newIndex = oldIndex;
+ }
+
+ _dispatchEvent(this, rootEl, 'end', dragEl, rootEl, oldIndex, newIndex);
+
+ // Save sorting
+ this.save();
+ }
+ }
+
+ }
+
+ this._nulling();
+ },
+
+ _nulling: function () {
+ rootEl =
+ dragEl =
+ parentEl =
+ ghostEl =
+ nextEl =
+ cloneEl =
+
+ scrollEl =
+ scrollParentEl =
+
+ tapEvt =
+ touchEvt =
+
+ moved =
+ newIndex =
+
+ lastEl =
+ lastCSS =
+
+ activeGroup =
+ Sortable.active = null;
+ },
+
+ handleEvent: function (/**Event*/evt) {
+ var type = evt.type;
+
+ if (type === 'dragover' || type === 'dragenter') {
+ if (dragEl) {
+ this._onDragOver(evt);
+ _globalDragOver(evt);
+ }
+ }
+ else if (type === 'drop' || type === 'dragend') {
+ this._onDrop(evt);
+ }
+ },
+
+
+ /**
+ * Serializes the item into an array of string.
+ * @returns {String[]}
+ */
+ toArray: function () {
+ var order = [],
+ el,
+ children = this.el.children,
+ i = 0,
+ n = children.length,
+ options = this.options;
+
+ for (; i < n; i++) {
+ el = children[i];
+ if (_closest(el, options.draggable, this.el)) {
+ order.push(el.getAttribute(options.dataIdAttr) || _generateId(el));
+ }
+ }
+
+ return order;
+ },
+
+
+ /**
+ * Sorts the elements according to the array.
+ * @param {String[]} order order of the items
+ */
+ sort: function (order) {
+ var items = {}, rootEl = this.el;
+
+ this.toArray().forEach(function (id, i) {
+ var el = rootEl.children[i];
+
+ if (_closest(el, this.options.draggable, rootEl)) {
+ items[id] = el;
+ }
+ }, this);
+
+ order.forEach(function (id) {
+ if (items[id]) {
+ rootEl.removeChild(items[id]);
+ rootEl.appendChild(items[id]);
+ }
+ });
+ },
+
+
+ /**
+ * Save the current sorting
+ */
+ save: function () {
+ var store = this.options.store;
+ store && store.set(this);
+ },
+
+
+ /**
+ * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.
+ * @param {HTMLElement} el
+ * @param {String} [selector] default: `options.draggable`
+ * @returns {HTMLElement|null}
+ */
+ closest: function (el, selector) {
+ return _closest(el, selector || this.options.draggable, this.el);
+ },
+
+
+ /**
+ * Set/get option
+ * @param {string} name
+ * @param {*} [value]
+ * @returns {*}
+ */
+ option: function (name, value) {
+ var options = this.options;
+
+ if (value === void 0) {
+ return options[name];
+ } else {
+ options[name] = value;
+
+ if (name === 'group') {
+ _prepareGroup(options);
+ }
+ }
+ },
+
+
+ /**
+ * Destroy
+ */
+ destroy: function () {
+ var el = this.el;
+
+ el[expando] = null;
+
+ _off(el, 'mousedown', this._onTapStart);
+ _off(el, 'touchstart', this._onTapStart);
+
+ if (this.nativeDraggable) {
+ _off(el, 'dragover', this);
+ _off(el, 'dragenter', this);
+ }
+
+ // Remove draggable attributes
+ Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) {
+ el.removeAttribute('draggable');
+ });
+
+ touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1);
+
+ this._onDrop();
+
+ this.el = el = null;
+ }
+ };
+
+
+ function _cloneHide(state) {
+ if (cloneEl && (cloneEl.state !== state)) {
+ _css(cloneEl, 'display', state ? 'none' : '');
+ !state && cloneEl.state && rootEl.insertBefore(cloneEl, dragEl);
+ cloneEl.state = state;
+ }
+ }
+
+
+ function _closest(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/ctx) {
+ if (el) {
+ ctx = ctx || document;
+
+ do {
+ if ((selector === '>*' && el.parentNode === ctx) || _matches(el, selector)) {
+ return el;
+ }
+ }
+ while (el !== ctx && (el = el.parentNode));
+ }
+
+ return null;
+ }
+
+
+ function _globalDragOver(/**Event*/evt) {
+ if (evt.dataTransfer) {
+ evt.dataTransfer.dropEffect = 'move';
+ }
+ evt.preventDefault();
+ }
+
+
+ function _on(el, event, fn) {
+ el.addEventListener(event, fn, false);
+ }
+
+
+ function _off(el, event, fn) {
+ el.removeEventListener(event, fn, false);
+ }
+
+
+ function _toggleClass(el, name, state) {
+ if (el) {
+ if (el.classList) {
+ el.classList[state ? 'add' : 'remove'](name);
+ }
+ else {
+ var className = (' ' + el.className + ' ').replace(RSPACE, ' ').replace(' ' + name + ' ', ' ');
+ el.className = (className + (state ? ' ' + name : '')).replace(RSPACE, ' ');
+ }
+ }
+ }
+
+
+ function _css(el, prop, val) {
+ var style = el && el.style;
+
+ if (style) {
+ if (val === void 0) {
+ if (document.defaultView && document.defaultView.getComputedStyle) {
+ val = document.defaultView.getComputedStyle(el, '');
+ }
+ else if (el.currentStyle) {
+ val = el.currentStyle;
+ }
+
+ return prop === void 0 ? val : val[prop];
+ }
+ else {
+ if (!(prop in style)) {
+ prop = '-webkit-' + prop;
+ }
+
+ style[prop] = val + (typeof val === 'string' ? '' : 'px');
+ }
+ }
+ }
+
+
+ function _find(ctx, tagName, iterator) {
+ if (ctx) {
+ var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length;
+
+ if (iterator) {
+ for (; i < n; i++) {
+ iterator(list[i], i);
+ }
+ }
+
+ return list;
+ }
+
+ return [];
+ }
+
+
+
+ function _dispatchEvent(sortable, rootEl, name, targetEl, fromEl, startIndex, newIndex) {
+ var evt = document.createEvent('Event'),
+ options = (sortable || rootEl[expando]).options,
+ onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1);
+
+ evt.initEvent(name, true, true);
+
+ evt.to = rootEl;
+ evt.from = fromEl || rootEl;
+ evt.item = targetEl || rootEl;
+ evt.clone = cloneEl;
+
+ evt.oldIndex = startIndex;
+ evt.newIndex = newIndex;
+
+ rootEl.dispatchEvent(evt);
+
+ if (options[onName]) {
+ options[onName].call(sortable, evt);
+ }
+ }
+
+
+ function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect) {
+ var evt,
+ sortable = fromEl[expando],
+ onMoveFn = sortable.options.onMove,
+ retVal;
+
+ evt = document.createEvent('Event');
+ evt.initEvent('move', true, true);
+
+ evt.to = toEl;
+ evt.from = fromEl;
+ evt.dragged = dragEl;
+ evt.draggedRect = dragRect;
+ evt.related = targetEl || toEl;
+ evt.relatedRect = targetRect || toEl.getBoundingClientRect();
+
+ fromEl.dispatchEvent(evt);
+
+ if (onMoveFn) {
+ retVal = onMoveFn.call(sortable, evt);
+ }
+
+ return retVal;
+ }
+
+
+ function _disableDraggable(el) {
+ el.draggable = false;
+ }
+
+
+ function _unsilent() {
+ _silent = false;
+ }
+
+
+ /** @returns {HTMLElement|false} */
+ function _ghostIsLast(el, evt) {
+ var lastEl = el.lastElementChild,
+ rect = lastEl.getBoundingClientRect();
+
+ return ((evt.clientY - (rect.top + rect.height) > 5) || (evt.clientX - (rect.right + rect.width) > 5)) && lastEl; // min delta
+ }
+
+
+ /**
+ * Generate id
+ * @param {HTMLElement} el
+ * @returns {String}
+ * @private
+ */
+ function _generateId(el) {
+ var str = el.tagName + el.className + el.src + el.href + el.textContent,
+ i = str.length,
+ sum = 0;
+
+ while (i--) {
+ sum += str.charCodeAt(i);
+ }
+
+ return sum.toString(36);
+ }
+
+ /**
+ * Returns the index of an element within its parent for a selected set of
+ * elements
+ * @param {HTMLElement} el
+ * @param {selector} selector
+ * @return {number}
+ */
+ function _index(el, selector) {
+ var index = 0;
+
+ if (!el || !el.parentNode) {
+ return -1;
+ }
+
+ while (el && (el = el.previousElementSibling)) {
+ if ((el.nodeName.toUpperCase() !== 'TEMPLATE') && (selector === '>*' || _matches(el, selector))) {
+ index++;
+ }
+ }
+
+ return index;
+ }
+
+ function _matches(/**HTMLElement*/el, /**String*/selector) {
+ if (el) {
+ selector = selector.split('.');
+
+ var tag = selector.shift().toUpperCase(),
+ re = new RegExp('\\s(' + selector.join('|') + ')(?=\\s)', 'g');
+
+ return (
+ (tag === '' || el.nodeName.toUpperCase() == tag) &&
+ (!selector.length || ((' ' + el.className + ' ').match(re) || []).length == selector.length)
+ );
+ }
+
+ return false;
+ }
+
+ function _throttle(callback, ms) {
+ var args, _this;
+
+ return function () {
+ if (args === void 0) {
+ args = arguments;
+ _this = this;
+
+ setTimeout(function () {
+ if (args.length === 1) {
+ callback.call(_this, args[0]);
+ } else {
+ callback.apply(_this, args);
+ }
+
+ args = void 0;
+ }, ms);
+ }
+ };
+ }
+
+ function _extend(dst, src) {
+ if (dst && src) {
+ for (var key in src) {
+ if (src.hasOwnProperty(key)) {
+ dst[key] = src[key];
+ }
+ }
+ }
+
+ return dst;
+ }
+
+
+ // Export utils
+ Sortable.utils = {
+ on: _on,
+ off: _off,
+ css: _css,
+ find: _find,
+ is: function (el, selector) {
+ return !!_closest(el, selector, el);
+ },
+ extend: _extend,
+ throttle: _throttle,
+ closest: _closest,
+ toggleClass: _toggleClass,
+ index: _index
+ };
+
+
+ /**
+ * Create sortable instance
+ * @param {HTMLElement} el
+ * @param {Object} [options]
+ */
+ Sortable.create = function (el, options) {
+ return new Sortable(el, options);
+ };
+
+
+ // Export
+ Sortable.version = '1.4.2';
+ return Sortable;
+});
diff --git a/vendor/assets/javascripts/clipboard.js b/vendor/assets/javascripts/clipboard.js
index 1b1f4f0bd63..39d7d2306f8 100644
--- a/vendor/assets/javascripts/clipboard.js
+++ b/vendor/assets/javascripts/clipboard.js
@@ -154,12 +154,12 @@ function E () {
E.prototype = {
on: function (name, callback, ctx) {
var e = this.e || (this.e = {});
-
+
(e[name] || (e[name] = [])).push({
fn: callback,
ctx: ctx
});
-
+
return this;
},
@@ -169,7 +169,7 @@ E.prototype = {
self.off(name, fn);
callback.apply(ctx, arguments);
};
-
+
return this.on(name, fn, ctx);
},
@@ -178,11 +178,11 @@ E.prototype = {
var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
var i = 0;
var len = evtArr.length;
-
+
for (i; i < len; i++) {
evtArr[i].fn.apply(evtArr[i].ctx, data);
}
-
+
return this;
},
@@ -190,21 +190,21 @@ E.prototype = {
var e = this.e || (this.e = {});
var evts = e[name];
var liveEvents = [];
-
+
if (evts && callback) {
for (var i = 0, len = evts.length; i < len; i++) {
if (evts[i].fn !== callback) liveEvents.push(evts[i]);
}
}
-
+
// Remove event from queue to prevent memory leak
// Suggested by https://github.com/lazd
// Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
- (liveEvents.length)
+ (liveEvents.length)
? e[name] = liveEvents
: delete e[name];
-
+
return this;
}
};
@@ -618,4 +618,4 @@ exports['default'] = Clipboard;
module.exports = exports['default'];
},{"./clipboard-action":6,"delegate-events":1,"tiny-emitter":5}]},{},[7])(7)
-}); \ No newline at end of file
+});
diff --git a/vendor/assets/javascripts/vue-resource.full.js b/vendor/assets/javascripts/vue-resource.full.js
new file mode 100644
index 00000000000..d7981dbec7e
--- /dev/null
+++ b/vendor/assets/javascripts/vue-resource.full.js
@@ -0,0 +1,1318 @@
+/*!
+ * vue-resource v0.9.3
+ * https://github.com/vuejs/vue-resource
+ * Released under the MIT License.
+ */
+
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.VueResource = factory());
+}(this, function () { 'use strict';
+
+ /**
+ * Promises/A+ polyfill v1.1.4 (https://github.com/bramstein/promis)
+ */
+
+ var RESOLVED = 0;
+ var REJECTED = 1;
+ var PENDING = 2;
+
+ function Promise$2(executor) {
+
+ this.state = PENDING;
+ this.value = undefined;
+ this.deferred = [];
+
+ var promise = this;
+
+ try {
+ executor(function (x) {
+ promise.resolve(x);
+ }, function (r) {
+ promise.reject(r);
+ });
+ } catch (e) {
+ promise.reject(e);
+ }
+ }
+
+ Promise$2.reject = function (r) {
+ return new Promise$2(function (resolve, reject) {
+ reject(r);
+ });
+ };
+
+ Promise$2.resolve = function (x) {
+ return new Promise$2(function (resolve, reject) {
+ resolve(x);
+ });
+ };
+
+ Promise$2.all = function all(iterable) {
+ return new Promise$2(function (resolve, reject) {
+ var count = 0,
+ result = [];
+
+ if (iterable.length === 0) {
+ resolve(result);
+ }
+
+ function resolver(i) {
+ return function (x) {
+ result[i] = x;
+ count += 1;
+
+ if (count === iterable.length) {
+ resolve(result);
+ }
+ };
+ }
+
+ for (var i = 0; i < iterable.length; i += 1) {
+ Promise$2.resolve(iterable[i]).then(resolver(i), reject);
+ }
+ });
+ };
+
+ Promise$2.race = function race(iterable) {
+ return new Promise$2(function (resolve, reject) {
+ for (var i = 0; i < iterable.length; i += 1) {
+ Promise$2.resolve(iterable[i]).then(resolve, reject);
+ }
+ });
+ };
+
+ var p$1 = Promise$2.prototype;
+
+ p$1.resolve = function resolve(x) {
+ var promise = this;
+
+ if (promise.state === PENDING) {
+ if (x === promise) {
+ throw new TypeError('Promise settled with itself.');
+ }
+
+ var called = false;
+
+ try {
+ var then = x && x['then'];
+
+ if (x !== null && typeof x === 'object' && typeof then === 'function') {
+ then.call(x, function (x) {
+ if (!called) {
+ promise.resolve(x);
+ }
+ called = true;
+ }, function (r) {
+ if (!called) {
+ promise.reject(r);
+ }
+ called = true;
+ });
+ return;
+ }
+ } catch (e) {
+ if (!called) {
+ promise.reject(e);
+ }
+ return;
+ }
+
+ promise.state = RESOLVED;
+ promise.value = x;
+ promise.notify();
+ }
+ };
+
+ p$1.reject = function reject(reason) {
+ var promise = this;
+
+ if (promise.state === PENDING) {
+ if (reason === promise) {
+ throw new TypeError('Promise settled with itself.');
+ }
+
+ promise.state = REJECTED;
+ promise.value = reason;
+ promise.notify();
+ }
+ };
+
+ p$1.notify = function notify() {
+ var promise = this;
+
+ nextTick(function () {
+ if (promise.state !== PENDING) {
+ while (promise.deferred.length) {
+ var deferred = promise.deferred.shift(),
+ onResolved = deferred[0],
+ onRejected = deferred[1],
+ resolve = deferred[2],
+ reject = deferred[3];
+
+ try {
+ if (promise.state === RESOLVED) {
+ if (typeof onResolved === 'function') {
+ resolve(onResolved.call(undefined, promise.value));
+ } else {
+ resolve(promise.value);
+ }
+ } else if (promise.state === REJECTED) {
+ if (typeof onRejected === 'function') {
+ resolve(onRejected.call(undefined, promise.value));
+ } else {
+ reject(promise.value);
+ }
+ }
+ } catch (e) {
+ reject(e);
+ }
+ }
+ }
+ });
+ };
+
+ p$1.then = function then(onResolved, onRejected) {
+ var promise = this;
+
+ return new Promise$2(function (resolve, reject) {
+ promise.deferred.push([onResolved, onRejected, resolve, reject]);
+ promise.notify();
+ });
+ };
+
+ p$1.catch = function (onRejected) {
+ return this.then(undefined, onRejected);
+ };
+
+ var PromiseObj = window.Promise || Promise$2;
+
+ function Promise$1(executor, context) {
+
+ if (executor instanceof PromiseObj) {
+ this.promise = executor;
+ } else {
+ this.promise = new PromiseObj(executor.bind(context));
+ }
+
+ this.context = context;
+ }
+
+ Promise$1.all = function (iterable, context) {
+ return new Promise$1(PromiseObj.all(iterable), context);
+ };
+
+ Promise$1.resolve = function (value, context) {
+ return new Promise$1(PromiseObj.resolve(value), context);
+ };
+
+ Promise$1.reject = function (reason, context) {
+ return new Promise$1(PromiseObj.reject(reason), context);
+ };
+
+ Promise$1.race = function (iterable, context) {
+ return new Promise$1(PromiseObj.race(iterable), context);
+ };
+
+ var p = Promise$1.prototype;
+
+ p.bind = function (context) {
+ this.context = context;
+ return this;
+ };
+
+ p.then = function (fulfilled, rejected) {
+
+ if (fulfilled && fulfilled.bind && this.context) {
+ fulfilled = fulfilled.bind(this.context);
+ }
+
+ if (rejected && rejected.bind && this.context) {
+ rejected = rejected.bind(this.context);
+ }
+
+ return new Promise$1(this.promise.then(fulfilled, rejected), this.context);
+ };
+
+ p.catch = function (rejected) {
+
+ if (rejected && rejected.bind && this.context) {
+ rejected = rejected.bind(this.context);
+ }
+
+ return new Promise$1(this.promise.catch(rejected), this.context);
+ };
+
+ p.finally = function (callback) {
+
+ return this.then(function (value) {
+ callback.call(this);
+ return value;
+ }, function (reason) {
+ callback.call(this);
+ return PromiseObj.reject(reason);
+ });
+ };
+
+ var debug = false;
+ var util = {};
+ var array = [];
+ function Util (Vue) {
+ util = Vue.util;
+ debug = Vue.config.debug || !Vue.config.silent;
+ }
+
+ function warn(msg) {
+ if (typeof console !== 'undefined' && debug) {
+ console.warn('[VueResource warn]: ' + msg);
+ }
+ }
+
+ function error(msg) {
+ if (typeof console !== 'undefined') {
+ console.error(msg);
+ }
+ }
+
+ function nextTick(cb, ctx) {
+ return util.nextTick(cb, ctx);
+ }
+
+ function trim(str) {
+ return str.replace(/^\s*|\s*$/g, '');
+ }
+
+ var isArray = Array.isArray;
+
+ function isString(val) {
+ return typeof val === 'string';
+ }
+
+ function isBoolean(val) {
+ return val === true || val === false;
+ }
+
+ function isFunction(val) {
+ return typeof val === 'function';
+ }
+
+ function isObject(obj) {
+ return obj !== null && typeof obj === 'object';
+ }
+
+ function isPlainObject(obj) {
+ return isObject(obj) && Object.getPrototypeOf(obj) == Object.prototype;
+ }
+
+ function isFormData(obj) {
+ return typeof FormData !== 'undefined' && obj instanceof FormData;
+ }
+
+ function when(value, fulfilled, rejected) {
+
+ var promise = Promise$1.resolve(value);
+
+ if (arguments.length < 2) {
+ return promise;
+ }
+
+ return promise.then(fulfilled, rejected);
+ }
+
+ function options(fn, obj, opts) {
+
+ opts = opts || {};
+
+ if (isFunction(opts)) {
+ opts = opts.call(obj);
+ }
+
+ return merge(fn.bind({ $vm: obj, $options: opts }), fn, { $options: opts });
+ }
+
+ function each(obj, iterator) {
+
+ var i, key;
+
+ if (typeof obj.length == 'number') {
+ for (i = 0; i < obj.length; i++) {
+ iterator.call(obj[i], obj[i], i);
+ }
+ } else if (isObject(obj)) {
+ for (key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ iterator.call(obj[key], obj[key], key);
+ }
+ }
+ }
+
+ return obj;
+ }
+
+ var assign = Object.assign || _assign;
+
+ function merge(target) {
+
+ var args = array.slice.call(arguments, 1);
+
+ args.forEach(function (source) {
+ _merge(target, source, true);
+ });
+
+ return target;
+ }
+
+ function defaults(target) {
+
+ var args = array.slice.call(arguments, 1);
+
+ args.forEach(function (source) {
+
+ for (var key in source) {
+ if (target[key] === undefined) {
+ target[key] = source[key];
+ }
+ }
+ });
+
+ return target;
+ }
+
+ function _assign(target) {
+
+ var args = array.slice.call(arguments, 1);
+
+ args.forEach(function (source) {
+ _merge(target, source);
+ });
+
+ return target;
+ }
+
+ function _merge(target, source, deep) {
+ for (var key in source) {
+ if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
+ if (isPlainObject(source[key]) && !isPlainObject(target[key])) {
+ target[key] = {};
+ }
+ if (isArray(source[key]) && !isArray(target[key])) {
+ target[key] = [];
+ }
+ _merge(target[key], source[key], deep);
+ } else if (source[key] !== undefined) {
+ target[key] = source[key];
+ }
+ }
+ }
+
+ function root (options, next) {
+
+ var url = next(options);
+
+ if (isString(options.root) && !url.match(/^(https?:)?\//)) {
+ url = options.root + '/' + url;
+ }
+
+ return url;
+ }
+
+ function query (options, next) {
+
+ var urlParams = Object.keys(Url.options.params),
+ query = {},
+ url = next(options);
+
+ each(options.params, function (value, key) {
+ if (urlParams.indexOf(key) === -1) {
+ query[key] = value;
+ }
+ });
+
+ query = Url.params(query);
+
+ if (query) {
+ url += (url.indexOf('?') == -1 ? '?' : '&') + query;
+ }
+
+ return url;
+ }
+
+ /**
+ * URL Template v2.0.6 (https://github.com/bramstein/url-template)
+ */
+
+ function expand(url, params, variables) {
+
+ var tmpl = parse(url),
+ expanded = tmpl.expand(params);
+
+ if (variables) {
+ variables.push.apply(variables, tmpl.vars);
+ }
+
+ return expanded;
+ }
+
+ function parse(template) {
+
+ var operators = ['+', '#', '.', '/', ';', '?', '&'],
+ variables = [];
+
+ return {
+ vars: variables,
+ expand: function (context) {
+ return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, function (_, expression, literal) {
+ if (expression) {
+
+ var operator = null,
+ values = [];
+
+ if (operators.indexOf(expression.charAt(0)) !== -1) {
+ operator = expression.charAt(0);
+ expression = expression.substr(1);
+ }
+
+ expression.split(/,/g).forEach(function (variable) {
+ var tmp = /([^:\*]*)(?::(\d+)|(\*))?/.exec(variable);
+ values.push.apply(values, getValues(context, operator, tmp[1], tmp[2] || tmp[3]));
+ variables.push(tmp[1]);
+ });
+
+ if (operator && operator !== '+') {
+
+ var separator = ',';
+
+ if (operator === '?') {
+ separator = '&';
+ } else if (operator !== '#') {
+ separator = operator;
+ }
+
+ return (values.length !== 0 ? operator : '') + values.join(separator);
+ } else {
+ return values.join(',');
+ }
+ } else {
+ return encodeReserved(literal);
+ }
+ });
+ }
+ };
+ }
+
+ function getValues(context, operator, key, modifier) {
+
+ var value = context[key],
+ result = [];
+
+ if (isDefined(value) && value !== '') {
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
+ value = value.toString();
+
+ if (modifier && modifier !== '*') {
+ value = value.substring(0, parseInt(modifier, 10));
+ }
+
+ result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : null));
+ } else {
+ if (modifier === '*') {
+ if (Array.isArray(value)) {
+ value.filter(isDefined).forEach(function (value) {
+ result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : null));
+ });
+ } else {
+ Object.keys(value).forEach(function (k) {
+ if (isDefined(value[k])) {
+ result.push(encodeValue(operator, value[k], k));
+ }
+ });
+ }
+ } else {
+ var tmp = [];
+
+ if (Array.isArray(value)) {
+ value.filter(isDefined).forEach(function (value) {
+ tmp.push(encodeValue(operator, value));
+ });
+ } else {
+ Object.keys(value).forEach(function (k) {
+ if (isDefined(value[k])) {
+ tmp.push(encodeURIComponent(k));
+ tmp.push(encodeValue(operator, value[k].toString()));
+ }
+ });
+ }
+
+ if (isKeyOperator(operator)) {
+ result.push(encodeURIComponent(key) + '=' + tmp.join(','));
+ } else if (tmp.length !== 0) {
+ result.push(tmp.join(','));
+ }
+ }
+ }
+ } else {
+ if (operator === ';') {
+ result.push(encodeURIComponent(key));
+ } else if (value === '' && (operator === '&' || operator === '?')) {
+ result.push(encodeURIComponent(key) + '=');
+ } else if (value === '') {
+ result.push('');
+ }
+ }
+
+ return result;
+ }
+
+ function isDefined(value) {
+ return value !== undefined && value !== null;
+ }
+
+ function isKeyOperator(operator) {
+ return operator === ';' || operator === '&' || operator === '?';
+ }
+
+ function encodeValue(operator, value, key) {
+
+ value = operator === '+' || operator === '#' ? encodeReserved(value) : encodeURIComponent(value);
+
+ if (key) {
+ return encodeURIComponent(key) + '=' + value;
+ } else {
+ return value;
+ }
+ }
+
+ function encodeReserved(str) {
+ return str.split(/(%[0-9A-Fa-f]{2})/g).map(function (part) {
+ if (!/%[0-9A-Fa-f]/.test(part)) {
+ part = encodeURI(part);
+ }
+ return part;
+ }).join('');
+ }
+
+ function template (options) {
+
+ var variables = [],
+ url = expand(options.url, options.params, variables);
+
+ variables.forEach(function (key) {
+ delete options.params[key];
+ });
+
+ return url;
+ }
+
+ /**
+ * Service for URL templating.
+ */
+
+ var ie = document.documentMode;
+ var el = document.createElement('a');
+
+ function Url(url, params) {
+
+ var self = this || {},
+ options = url,
+ transform;
+
+ if (isString(url)) {
+ options = { url: url, params: params };
+ }
+
+ options = merge({}, Url.options, self.$options, options);
+
+ Url.transforms.forEach(function (handler) {
+ transform = factory(handler, transform, self.$vm);
+ });
+
+ return transform(options);
+ }
+
+ /**
+ * Url options.
+ */
+
+ Url.options = {
+ url: '',
+ root: null,
+ params: {}
+ };
+
+ /**
+ * Url transforms.
+ */
+
+ Url.transforms = [template, query, root];
+
+ /**
+ * Encodes a Url parameter string.
+ *
+ * @param {Object} obj
+ */
+
+ Url.params = function (obj) {
+
+ var params = [],
+ escape = encodeURIComponent;
+
+ params.add = function (key, value) {
+
+ if (isFunction(value)) {
+ value = value();
+ }
+
+ if (value === null) {
+ value = '';
+ }
+
+ this.push(escape(key) + '=' + escape(value));
+ };
+
+ serialize(params, obj);
+
+ return params.join('&').replace(/%20/g, '+');
+ };
+
+ /**
+ * Parse a URL and return its components.
+ *
+ * @param {String} url
+ */
+
+ Url.parse = function (url) {
+
+ if (ie) {
+ el.href = url;
+ url = el.href;
+ }
+
+ el.href = url;
+
+ return {
+ href: el.href,
+ protocol: el.protocol ? el.protocol.replace(/:$/, '') : '',
+ port: el.port,
+ host: el.host,
+ hostname: el.hostname,
+ pathname: el.pathname.charAt(0) === '/' ? el.pathname : '/' + el.pathname,
+ search: el.search ? el.search.replace(/^\?/, '') : '',
+ hash: el.hash ? el.hash.replace(/^#/, '') : ''
+ };
+ };
+
+ function factory(handler, next, vm) {
+ return function (options) {
+ return handler.call(vm, options, next);
+ };
+ }
+
+ function serialize(params, obj, scope) {
+
+ var array = isArray(obj),
+ plain = isPlainObject(obj),
+ hash;
+
+ each(obj, function (value, key) {
+
+ hash = isObject(value) || isArray(value);
+
+ if (scope) {
+ key = scope + '[' + (plain || hash ? key : '') + ']';
+ }
+
+ if (!scope && array) {
+ params.add(value.name, value.value);
+ } else if (hash) {
+ serialize(params, value, key);
+ } else {
+ params.add(key, value);
+ }
+ });
+ }
+
+ function xdrClient (request) {
+ return new Promise$1(function (resolve) {
+
+ var xdr = new XDomainRequest(),
+ handler = function (event) {
+
+ var response = request.respondWith(xdr.responseText, {
+ status: xdr.status,
+ statusText: xdr.statusText
+ });
+
+ resolve(response);
+ };
+
+ request.abort = function () {
+ return xdr.abort();
+ };
+
+ xdr.open(request.method, request.getUrl(), true);
+ xdr.timeout = 0;
+ xdr.onload = handler;
+ xdr.onerror = handler;
+ xdr.ontimeout = function () {};
+ xdr.onprogress = function () {};
+ xdr.send(request.getBody());
+ });
+ }
+
+ var ORIGIN_URL = Url.parse(location.href);
+ var SUPPORTS_CORS = 'withCredentials' in new XMLHttpRequest();
+
+ function cors (request, next) {
+
+ if (!isBoolean(request.crossOrigin) && crossOrigin(request)) {
+ request.crossOrigin = true;
+ }
+
+ if (request.crossOrigin) {
+
+ if (!SUPPORTS_CORS) {
+ request.client = xdrClient;
+ }
+
+ delete request.emulateHTTP;
+ }
+
+ next();
+ }
+
+ function crossOrigin(request) {
+
+ var requestUrl = Url.parse(Url(request));
+
+ return requestUrl.protocol !== ORIGIN_URL.protocol || requestUrl.host !== ORIGIN_URL.host;
+ }
+
+ function body (request, next) {
+
+ if (request.emulateJSON && isPlainObject(request.body)) {
+ request.body = Url.params(request.body);
+ request.headers['Content-Type'] = 'application/x-www-form-urlencoded';
+ }
+
+ if (isFormData(request.body)) {
+ delete request.headers['Content-Type'];
+ }
+
+ if (isPlainObject(request.body)) {
+ request.body = JSON.stringify(request.body);
+ }
+
+ next(function (response) {
+
+ var contentType = response.headers['Content-Type'];
+
+ if (isString(contentType) && contentType.indexOf('application/json') === 0) {
+
+ try {
+ response.data = response.json();
+ } catch (e) {
+ response.data = null;
+ }
+ } else {
+ response.data = response.text();
+ }
+ });
+ }
+
+ function jsonpClient (request) {
+ return new Promise$1(function (resolve) {
+
+ var name = request.jsonp || 'callback',
+ callback = '_jsonp' + Math.random().toString(36).substr(2),
+ body = null,
+ handler,
+ script;
+
+ handler = function (event) {
+
+ var status = 0;
+
+ if (event.type === 'load' && body !== null) {
+ status = 200;
+ } else if (event.type === 'error') {
+ status = 404;
+ }
+
+ resolve(request.respondWith(body, { status: status }));
+
+ delete window[callback];
+ document.body.removeChild(script);
+ };
+
+ request.params[name] = callback;
+
+ window[callback] = function (result) {
+ body = JSON.stringify(result);
+ };
+
+ script = document.createElement('script');
+ script.src = request.getUrl();
+ script.type = 'text/javascript';
+ script.async = true;
+ script.onload = handler;
+ script.onerror = handler;
+
+ document.body.appendChild(script);
+ });
+ }
+
+ function jsonp (request, next) {
+
+ if (request.method == 'JSONP') {
+ request.client = jsonpClient;
+ }
+
+ next(function (response) {
+
+ if (request.method == 'JSONP') {
+ response.data = response.json();
+ }
+ });
+ }
+
+ function before (request, next) {
+
+ if (isFunction(request.before)) {
+ request.before.call(this, request);
+ }
+
+ next();
+ }
+
+ /**
+ * HTTP method override Interceptor.
+ */
+
+ function method (request, next) {
+
+ if (request.emulateHTTP && /^(PUT|PATCH|DELETE)$/i.test(request.method)) {
+ request.headers['X-HTTP-Method-Override'] = request.method;
+ request.method = 'POST';
+ }
+
+ next();
+ }
+
+ function header (request, next) {
+
+ request.method = request.method.toUpperCase();
+ request.headers = assign({}, Http.headers.common, !request.crossOrigin ? Http.headers.custom : {}, Http.headers[request.method.toLowerCase()], request.headers);
+
+ next();
+ }
+
+ /**
+ * Timeout Interceptor.
+ */
+
+ function timeout (request, next) {
+
+ var timeout;
+
+ if (request.timeout) {
+ timeout = setTimeout(function () {
+ request.abort();
+ }, request.timeout);
+ }
+
+ next(function (response) {
+
+ clearTimeout(timeout);
+ });
+ }
+
+ function xhrClient (request) {
+ return new Promise$1(function (resolve) {
+
+ var xhr = new XMLHttpRequest(),
+ handler = function (event) {
+
+ var response = request.respondWith('response' in xhr ? xhr.response : xhr.responseText, {
+ status: xhr.status === 1223 ? 204 : xhr.status, // IE9 status bug
+ statusText: xhr.status === 1223 ? 'No Content' : trim(xhr.statusText),
+ headers: parseHeaders(xhr.getAllResponseHeaders())
+ });
+
+ resolve(response);
+ };
+
+ request.abort = function () {
+ return xhr.abort();
+ };
+
+ xhr.open(request.method, request.getUrl(), true);
+ xhr.timeout = 0;
+ xhr.onload = handler;
+ xhr.onerror = handler;
+
+ if (request.progress) {
+ if (request.method === 'GET') {
+ xhr.addEventListener('progress', request.progress);
+ } else if (/^(POST|PUT)$/i.test(request.method)) {
+ xhr.upload.addEventListener('progress', request.progress);
+ }
+ }
+
+ if (request.credentials === true) {
+ xhr.withCredentials = true;
+ }
+
+ each(request.headers || {}, function (value, header) {
+ xhr.setRequestHeader(header, value);
+ });
+
+ xhr.send(request.getBody());
+ });
+ }
+
+ function parseHeaders(str) {
+
+ var headers = {},
+ value,
+ name,
+ i;
+
+ each(trim(str).split('\n'), function (row) {
+
+ i = row.indexOf(':');
+ name = trim(row.slice(0, i));
+ value = trim(row.slice(i + 1));
+
+ if (headers[name]) {
+
+ if (isArray(headers[name])) {
+ headers[name].push(value);
+ } else {
+ headers[name] = [headers[name], value];
+ }
+ } else {
+
+ headers[name] = value;
+ }
+ });
+
+ return headers;
+ }
+
+ function Client (context) {
+
+ var reqHandlers = [sendRequest],
+ resHandlers = [],
+ handler;
+
+ if (!isObject(context)) {
+ context = null;
+ }
+
+ function Client(request) {
+ return new Promise$1(function (resolve) {
+
+ function exec() {
+
+ handler = reqHandlers.pop();
+
+ if (isFunction(handler)) {
+ handler.call(context, request, next);
+ } else {
+ warn('Invalid interceptor of type ' + typeof handler + ', must be a function');
+ next();
+ }
+ }
+
+ function next(response) {
+
+ if (isFunction(response)) {
+
+ resHandlers.unshift(response);
+ } else if (isObject(response)) {
+
+ resHandlers.forEach(function (handler) {
+ response = when(response, function (response) {
+ return handler.call(context, response) || response;
+ });
+ });
+
+ when(response, resolve);
+
+ return;
+ }
+
+ exec();
+ }
+
+ exec();
+ }, context);
+ }
+
+ Client.use = function (handler) {
+ reqHandlers.push(handler);
+ };
+
+ return Client;
+ }
+
+ function sendRequest(request, resolve) {
+
+ var client = request.client || xhrClient;
+
+ resolve(client(request));
+ }
+
+ var classCallCheck = function (instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+ };
+
+ /**
+ * HTTP Response.
+ */
+
+ var Response = function () {
+ function Response(body, _ref) {
+ var url = _ref.url;
+ var headers = _ref.headers;
+ var status = _ref.status;
+ var statusText = _ref.statusText;
+ classCallCheck(this, Response);
+
+
+ this.url = url;
+ this.body = body;
+ this.headers = headers || {};
+ this.status = status || 0;
+ this.statusText = statusText || '';
+ this.ok = status >= 200 && status < 300;
+ }
+
+ Response.prototype.text = function text() {
+ return this.body;
+ };
+
+ Response.prototype.blob = function blob() {
+ return new Blob([this.body]);
+ };
+
+ Response.prototype.json = function json() {
+ return JSON.parse(this.body);
+ };
+
+ return Response;
+ }();
+
+ var Request = function () {
+ function Request(options) {
+ classCallCheck(this, Request);
+
+
+ this.method = 'GET';
+ this.body = null;
+ this.params = {};
+ this.headers = {};
+
+ assign(this, options);
+ }
+
+ Request.prototype.getUrl = function getUrl() {
+ return Url(this);
+ };
+
+ Request.prototype.getBody = function getBody() {
+ return this.body;
+ };
+
+ Request.prototype.respondWith = function respondWith(body, options) {
+ return new Response(body, assign(options || {}, { url: this.getUrl() }));
+ };
+
+ return Request;
+ }();
+
+ /**
+ * Service for sending network requests.
+ */
+
+ var CUSTOM_HEADERS = { 'X-Requested-With': 'XMLHttpRequest' };
+ var COMMON_HEADERS = { 'Accept': 'application/json, text/plain, */*' };
+ var JSON_CONTENT_TYPE = { 'Content-Type': 'application/json;charset=utf-8' };
+
+ function Http(options) {
+
+ var self = this || {},
+ client = Client(self.$vm);
+
+ defaults(options || {}, self.$options, Http.options);
+
+ Http.interceptors.forEach(function (handler) {
+ client.use(handler);
+ });
+
+ return client(new Request(options)).then(function (response) {
+
+ return response.ok ? response : Promise$1.reject(response);
+ }, function (response) {
+
+ if (response instanceof Error) {
+ error(response);
+ }
+
+ return Promise$1.reject(response);
+ });
+ }
+
+ Http.options = {};
+
+ Http.headers = {
+ put: JSON_CONTENT_TYPE,
+ post: JSON_CONTENT_TYPE,
+ patch: JSON_CONTENT_TYPE,
+ delete: JSON_CONTENT_TYPE,
+ custom: CUSTOM_HEADERS,
+ common: COMMON_HEADERS
+ };
+
+ Http.interceptors = [before, timeout, method, body, jsonp, header, cors];
+
+ ['get', 'delete', 'head', 'jsonp'].forEach(function (method) {
+
+ Http[method] = function (url, options) {
+ return this(assign(options || {}, { url: url, method: method }));
+ };
+ });
+
+ ['post', 'put', 'patch'].forEach(function (method) {
+
+ Http[method] = function (url, body, options) {
+ return this(assign(options || {}, { url: url, method: method, body: body }));
+ };
+ });
+
+ function Resource(url, params, actions, options) {
+
+ var self = this || {},
+ resource = {};
+
+ actions = assign({}, Resource.actions, actions);
+
+ each(actions, function (action, name) {
+
+ action = merge({ url: url, params: params || {} }, options, action);
+
+ resource[name] = function () {
+ return (self.$http || Http)(opts(action, arguments));
+ };
+ });
+
+ return resource;
+ }
+
+ function opts(action, args) {
+
+ var options = assign({}, action),
+ params = {},
+ body;
+
+ switch (args.length) {
+
+ case 2:
+
+ params = args[0];
+ body = args[1];
+
+ break;
+
+ case 1:
+
+ if (/^(POST|PUT|PATCH)$/i.test(options.method)) {
+ body = args[0];
+ } else {
+ params = args[0];
+ }
+
+ break;
+
+ case 0:
+
+ break;
+
+ default:
+
+ throw 'Expected up to 4 arguments [params, body], got ' + args.length + ' arguments';
+ }
+
+ options.body = body;
+ options.params = assign({}, options.params, params);
+
+ return options;
+ }
+
+ Resource.actions = {
+
+ get: { method: 'GET' },
+ save: { method: 'POST' },
+ query: { method: 'GET' },
+ update: { method: 'PUT' },
+ remove: { method: 'DELETE' },
+ delete: { method: 'DELETE' }
+
+ };
+
+ function plugin(Vue) {
+
+ if (plugin.installed) {
+ return;
+ }
+
+ Util(Vue);
+
+ Vue.url = Url;
+ Vue.http = Http;
+ Vue.resource = Resource;
+ Vue.Promise = Promise$1;
+
+ Object.defineProperties(Vue.prototype, {
+
+ $url: {
+ get: function () {
+ return options(Vue.url, this, this.$options.url);
+ }
+ },
+
+ $http: {
+ get: function () {
+ return options(Vue.http, this, this.$options.http);
+ }
+ },
+
+ $resource: {
+ get: function () {
+ return Vue.resource.bind(this);
+ }
+ },
+
+ $promise: {
+ get: function () {
+ var _this = this;
+
+ return function (executor) {
+ return new Vue.Promise(executor, _this);
+ };
+ }
+ }
+
+ });
+ }
+
+ if (typeof window !== 'undefined' && window.Vue) {
+ window.Vue.use(plugin);
+ }
+
+ return plugin;
+
+})); \ No newline at end of file
diff --git a/vendor/assets/javascripts/vue-resource.js.erb b/vendor/assets/javascripts/vue-resource.js.erb
new file mode 100644
index 00000000000..8001775ce98
--- /dev/null
+++ b/vendor/assets/javascripts/vue-resource.js.erb
@@ -0,0 +1,2 @@
+<% type = Rails.env.development? ? 'full' : 'min' %>
+<%= File.read(Rails.root.join("vendor/assets/javascripts/vue-resource.#{type}.js")) %>
diff --git a/vendor/assets/javascripts/vue-resource.min.js b/vendor/assets/javascripts/vue-resource.min.js
new file mode 100644
index 00000000000..6bff73a2a67
--- /dev/null
+++ b/vendor/assets/javascripts/vue-resource.min.js
@@ -0,0 +1,7 @@
+/*!
+ * vue-resource v0.9.3
+ * https://github.com/vuejs/vue-resource
+ * Released under the MIT License.
+ */
+
+!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):t.VueResource=n()}(this,function(){"use strict";function t(t){this.state=Z,this.value=void 0,this.deferred=[];var n=this;try{t(function(t){n.resolve(t)},function(t){n.reject(t)})}catch(e){n.reject(e)}}function n(t,n){t instanceof nt?this.promise=t:this.promise=new nt(t.bind(n)),this.context=n}function e(t){rt=t.util,ot=t.config.debug||!t.config.silent}function o(t){"undefined"!=typeof console&&ot&&console.warn("[VueResource warn]: "+t)}function r(t){"undefined"!=typeof console&&console.error(t)}function i(t,n){return rt.nextTick(t,n)}function u(t){return t.replace(/^\s*|\s*$/g,"")}function s(t){return"string"==typeof t}function c(t){return t===!0||t===!1}function a(t){return"function"==typeof t}function f(t){return null!==t&&"object"==typeof t}function h(t){return f(t)&&Object.getPrototypeOf(t)==Object.prototype}function p(t){return"undefined"!=typeof FormData&&t instanceof FormData}function l(t,e,o){var r=n.resolve(t);return arguments.length<2?r:r.then(e,o)}function d(t,n,e){return e=e||{},a(e)&&(e=e.call(n)),v(t.bind({$vm:n,$options:e}),t,{$options:e})}function m(t,n){var e,o;if("number"==typeof t.length)for(e=0;e<t.length;e++)n.call(t[e],t[e],e);else if(f(t))for(o in t)t.hasOwnProperty(o)&&n.call(t[o],t[o],o);return t}function v(t){var n=it.slice.call(arguments,1);return n.forEach(function(n){g(t,n,!0)}),t}function y(t){var n=it.slice.call(arguments,1);return n.forEach(function(n){for(var e in n)void 0===t[e]&&(t[e]=n[e])}),t}function b(t){var n=it.slice.call(arguments,1);return n.forEach(function(n){g(t,n)}),t}function g(t,n,e){for(var o in n)e&&(h(n[o])||ut(n[o]))?(h(n[o])&&!h(t[o])&&(t[o]={}),ut(n[o])&&!ut(t[o])&&(t[o]=[]),g(t[o],n[o],e)):void 0!==n[o]&&(t[o]=n[o])}function w(t,n){var e=n(t);return s(t.root)&&!e.match(/^(https?:)?\//)&&(e=t.root+"/"+e),e}function T(t,n){var e=Object.keys(R.options.params),o={},r=n(t);return m(t.params,function(t,n){e.indexOf(n)===-1&&(o[n]=t)}),o=R.params(o),o&&(r+=(r.indexOf("?")==-1?"?":"&")+o),r}function j(t,n,e){var o=E(t),r=o.expand(n);return e&&e.push.apply(e,o.vars),r}function E(t){var n=["+","#",".","/",";","?","&"],e=[];return{vars:e,expand:function(o){return t.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g,function(t,r,i){if(r){var u=null,s=[];if(n.indexOf(r.charAt(0))!==-1&&(u=r.charAt(0),r=r.substr(1)),r.split(/,/g).forEach(function(t){var n=/([^:\*]*)(?::(\d+)|(\*))?/.exec(t);s.push.apply(s,x(o,u,n[1],n[2]||n[3])),e.push(n[1])}),u&&"+"!==u){var c=",";return"?"===u?c="&":"#"!==u&&(c=u),(0!==s.length?u:"")+s.join(c)}return s.join(",")}return $(i)})}}}function x(t,n,e,o){var r=t[e],i=[];if(O(r)&&""!==r)if("string"==typeof r||"number"==typeof r||"boolean"==typeof r)r=r.toString(),o&&"*"!==o&&(r=r.substring(0,parseInt(o,10))),i.push(C(n,r,P(n)?e:null));else if("*"===o)Array.isArray(r)?r.filter(O).forEach(function(t){i.push(C(n,t,P(n)?e:null))}):Object.keys(r).forEach(function(t){O(r[t])&&i.push(C(n,r[t],t))});else{var u=[];Array.isArray(r)?r.filter(O).forEach(function(t){u.push(C(n,t))}):Object.keys(r).forEach(function(t){O(r[t])&&(u.push(encodeURIComponent(t)),u.push(C(n,r[t].toString())))}),P(n)?i.push(encodeURIComponent(e)+"="+u.join(",")):0!==u.length&&i.push(u.join(","))}else";"===n?i.push(encodeURIComponent(e)):""!==r||"&"!==n&&"?"!==n?""===r&&i.push(""):i.push(encodeURIComponent(e)+"=");return i}function O(t){return void 0!==t&&null!==t}function P(t){return";"===t||"&"===t||"?"===t}function C(t,n,e){return n="+"===t||"#"===t?$(n):encodeURIComponent(n),e?encodeURIComponent(e)+"="+n:n}function $(t){return t.split(/(%[0-9A-Fa-f]{2})/g).map(function(t){return/%[0-9A-Fa-f]/.test(t)||(t=encodeURI(t)),t}).join("")}function U(t){var n=[],e=j(t.url,t.params,n);return n.forEach(function(n){delete t.params[n]}),e}function R(t,n){var e,o=this||{},r=t;return s(t)&&(r={url:t,params:n}),r=v({},R.options,o.$options,r),R.transforms.forEach(function(t){e=A(t,e,o.$vm)}),e(r)}function A(t,n,e){return function(o){return t.call(e,o,n)}}function S(t,n,e){var o,r=ut(n),i=h(n);m(n,function(n,u){o=f(n)||ut(n),e&&(u=e+"["+(i||o?u:"")+"]"),!e&&r?t.add(n.name,n.value):o?S(t,n,u):t.add(u,n)})}function k(t){return new n(function(n){var e=new XDomainRequest,o=function(o){var r=t.respondWith(e.responseText,{status:e.status,statusText:e.statusText});n(r)};t.abort=function(){return e.abort()},e.open(t.method,t.getUrl(),!0),e.timeout=0,e.onload=o,e.onerror=o,e.ontimeout=function(){},e.onprogress=function(){},e.send(t.getBody())})}function H(t,n){!c(t.crossOrigin)&&I(t)&&(t.crossOrigin=!0),t.crossOrigin&&(ht||(t.client=k),delete t.emulateHTTP),n()}function I(t){var n=R.parse(R(t));return n.protocol!==ft.protocol||n.host!==ft.host}function L(t,n){t.emulateJSON&&h(t.body)&&(t.body=R.params(t.body),t.headers["Content-Type"]="application/x-www-form-urlencoded"),p(t.body)&&delete t.headers["Content-Type"],h(t.body)&&(t.body=JSON.stringify(t.body)),n(function(t){var n=t.headers["Content-Type"];if(s(n)&&0===n.indexOf("application/json"))try{t.data=t.json()}catch(e){t.data=null}else t.data=t.text()})}function q(t){return new n(function(n){var e,o,r=t.jsonp||"callback",i="_jsonp"+Math.random().toString(36).substr(2),u=null;e=function(e){var r=0;"load"===e.type&&null!==u?r=200:"error"===e.type&&(r=404),n(t.respondWith(u,{status:r})),delete window[i],document.body.removeChild(o)},t.params[r]=i,window[i]=function(t){u=JSON.stringify(t)},o=document.createElement("script"),o.src=t.getUrl(),o.type="text/javascript",o.async=!0,o.onload=e,o.onerror=e,document.body.appendChild(o)})}function N(t,n){"JSONP"==t.method&&(t.client=q),n(function(n){"JSONP"==t.method&&(n.data=n.json())})}function D(t,n){a(t.before)&&t.before.call(this,t),n()}function J(t,n){t.emulateHTTP&&/^(PUT|PATCH|DELETE)$/i.test(t.method)&&(t.headers["X-HTTP-Method-Override"]=t.method,t.method="POST"),n()}function M(t,n){t.method=t.method.toUpperCase(),t.headers=st({},V.headers.common,t.crossOrigin?{}:V.headers.custom,V.headers[t.method.toLowerCase()],t.headers),n()}function X(t,n){var e;t.timeout&&(e=setTimeout(function(){t.abort()},t.timeout)),n(function(t){clearTimeout(e)})}function W(t){return new n(function(n){var e=new XMLHttpRequest,o=function(o){var r=t.respondWith("response"in e?e.response:e.responseText,{status:1223===e.status?204:e.status,statusText:1223===e.status?"No Content":u(e.statusText),headers:B(e.getAllResponseHeaders())});n(r)};t.abort=function(){return e.abort()},e.open(t.method,t.getUrl(),!0),e.timeout=0,e.onload=o,e.onerror=o,t.progress&&("GET"===t.method?e.addEventListener("progress",t.progress):/^(POST|PUT)$/i.test(t.method)&&e.upload.addEventListener("progress",t.progress)),t.credentials===!0&&(e.withCredentials=!0),m(t.headers||{},function(t,n){e.setRequestHeader(n,t)}),e.send(t.getBody())})}function B(t){var n,e,o,r={};return m(u(t).split("\n"),function(t){o=t.indexOf(":"),e=u(t.slice(0,o)),n=u(t.slice(o+1)),r[e]?ut(r[e])?r[e].push(n):r[e]=[r[e],n]:r[e]=n}),r}function F(t){function e(e){return new n(function(n){function s(){r=i.pop(),a(r)?r.call(t,e,c):(o("Invalid interceptor of type "+typeof r+", must be a function"),c())}function c(e){if(a(e))u.unshift(e);else if(f(e))return u.forEach(function(n){e=l(e,function(e){return n.call(t,e)||e})}),void l(e,n);s()}s()},t)}var r,i=[G],u=[];return f(t)||(t=null),e.use=function(t){i.push(t)},e}function G(t,n){var e=t.client||W;n(e(t))}function V(t){var e=this||{},o=F(e.$vm);return y(t||{},e.$options,V.options),V.interceptors.forEach(function(t){o.use(t)}),o(new dt(t)).then(function(t){return t.ok?t:n.reject(t)},function(t){return t instanceof Error&&r(t),n.reject(t)})}function _(t,n,e,o){var r=this||{},i={};return e=st({},_.actions,e),m(e,function(e,u){e=v({url:t,params:n||{}},o,e),i[u]=function(){return(r.$http||V)(z(e,arguments))}}),i}function z(t,n){var e,o=st({},t),r={};switch(n.length){case 2:r=n[0],e=n[1];break;case 1:/^(POST|PUT|PATCH)$/i.test(o.method)?e=n[0]:r=n[0];break;case 0:break;default:throw"Expected up to 4 arguments [params, body], got "+n.length+" arguments"}return o.body=e,o.params=st({},o.params,r),o}function K(t){K.installed||(e(t),t.url=R,t.http=V,t.resource=_,t.Promise=n,Object.defineProperties(t.prototype,{$url:{get:function(){return d(t.url,this,this.$options.url)}},$http:{get:function(){return d(t.http,this,this.$options.http)}},$resource:{get:function(){return t.resource.bind(this)}},$promise:{get:function(){var n=this;return function(e){return new t.Promise(e,n)}}}}))}var Q=0,Y=1,Z=2;t.reject=function(n){return new t(function(t,e){e(n)})},t.resolve=function(n){return new t(function(t,e){t(n)})},t.all=function(n){return new t(function(e,o){function r(t){return function(o){u[t]=o,i+=1,i===n.length&&e(u)}}var i=0,u=[];0===n.length&&e(u);for(var s=0;s<n.length;s+=1)t.resolve(n[s]).then(r(s),o)})},t.race=function(n){return new t(function(e,o){for(var r=0;r<n.length;r+=1)t.resolve(n[r]).then(e,o)})};var tt=t.prototype;tt.resolve=function(t){var n=this;if(n.state===Z){if(t===n)throw new TypeError("Promise settled with itself.");var e=!1;try{var o=t&&t.then;if(null!==t&&"object"==typeof t&&"function"==typeof o)return void o.call(t,function(t){e||n.resolve(t),e=!0},function(t){e||n.reject(t),e=!0})}catch(r){return void(e||n.reject(r))}n.state=Q,n.value=t,n.notify()}},tt.reject=function(t){var n=this;if(n.state===Z){if(t===n)throw new TypeError("Promise settled with itself.");n.state=Y,n.value=t,n.notify()}},tt.notify=function(){var t=this;i(function(){if(t.state!==Z)for(;t.deferred.length;){var n=t.deferred.shift(),e=n[0],o=n[1],r=n[2],i=n[3];try{t.state===Q?r("function"==typeof e?e.call(void 0,t.value):t.value):t.state===Y&&("function"==typeof o?r(o.call(void 0,t.value)):i(t.value))}catch(u){i(u)}}})},tt.then=function(n,e){var o=this;return new t(function(t,r){o.deferred.push([n,e,t,r]),o.notify()})},tt["catch"]=function(t){return this.then(void 0,t)};var nt=window.Promise||t;n.all=function(t,e){return new n(nt.all(t),e)},n.resolve=function(t,e){return new n(nt.resolve(t),e)},n.reject=function(t,e){return new n(nt.reject(t),e)},n.race=function(t,e){return new n(nt.race(t),e)};var et=n.prototype;et.bind=function(t){return this.context=t,this},et.then=function(t,e){return t&&t.bind&&this.context&&(t=t.bind(this.context)),e&&e.bind&&this.context&&(e=e.bind(this.context)),new n(this.promise.then(t,e),this.context)},et["catch"]=function(t){return t&&t.bind&&this.context&&(t=t.bind(this.context)),new n(this.promise["catch"](t),this.context)},et["finally"]=function(t){return this.then(function(n){return t.call(this),n},function(n){return t.call(this),nt.reject(n)})};var ot=!1,rt={},it=[],ut=Array.isArray,st=Object.assign||b,ct=document.documentMode,at=document.createElement("a");R.options={url:"",root:null,params:{}},R.transforms=[U,T,w],R.params=function(t){var n=[],e=encodeURIComponent;return n.add=function(t,n){a(n)&&(n=n()),null===n&&(n=""),this.push(e(t)+"="+e(n))},S(n,t),n.join("&").replace(/%20/g,"+")},R.parse=function(t){return ct&&(at.href=t,t=at.href),at.href=t,{href:at.href,protocol:at.protocol?at.protocol.replace(/:$/,""):"",port:at.port,host:at.host,hostname:at.hostname,pathname:"/"===at.pathname.charAt(0)?at.pathname:"/"+at.pathname,search:at.search?at.search.replace(/^\?/,""):"",hash:at.hash?at.hash.replace(/^#/,""):""}};var ft=R.parse(location.href),ht="withCredentials"in new XMLHttpRequest,pt=function(t,n){if(!(t instanceof n))throw new TypeError("Cannot call a class as a function")},lt=function(){function t(n,e){var o=e.url,r=e.headers,i=e.status,u=e.statusText;pt(this,t),this.url=o,this.body=n,this.headers=r||{},this.status=i||0,this.statusText=u||"",this.ok=i>=200&&i<300}return t.prototype.text=function(){return this.body},t.prototype.blob=function(){return new Blob([this.body])},t.prototype.json=function(){return JSON.parse(this.body)},t}(),dt=function(){function t(n){pt(this,t),this.method="GET",this.body=null,this.params={},this.headers={},st(this,n)}return t.prototype.getUrl=function(){return R(this)},t.prototype.getBody=function(){return this.body},t.prototype.respondWith=function(t,n){return new lt(t,st(n||{},{url:this.getUrl()}))},t}(),mt={"X-Requested-With":"XMLHttpRequest"},vt={Accept:"application/json, text/plain, */*"},yt={"Content-Type":"application/json;charset=utf-8"};return V.options={},V.headers={put:yt,post:yt,patch:yt,"delete":yt,custom:mt,common:vt},V.interceptors=[D,X,J,L,N,M,H],["get","delete","head","jsonp"].forEach(function(t){V[t]=function(n,e){return this(st(e||{},{url:n,method:t}))}}),["post","put","patch"].forEach(function(t){V[t]=function(n,e,o){return this(st(o||{},{url:n,method:t,body:e}))}}),_.actions={get:{method:"GET"},save:{method:"POST"},query:{method:"GET"},update:{method:"PUT"},remove:{method:"DELETE"},"delete":{method:"DELETE"}},"undefined"!=typeof window&&window.Vue&&window.Vue.use(K),K}); \ No newline at end of file
diff --git a/vendor/assets/javascripts/vue.full.js b/vendor/assets/javascripts/vue.full.js
new file mode 100644
index 00000000000..7ae95897a01
--- /dev/null
+++ b/vendor/assets/javascripts/vue.full.js
@@ -0,0 +1,10073 @@
+/*!
+ * Vue.js v1.0.26
+ * (c) 2016 Evan You
+ * Released under the MIT License.
+ */
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.Vue = factory());
+}(this, function () { 'use strict';
+
+ function set(obj, key, val) {
+ if (hasOwn(obj, key)) {
+ obj[key] = val;
+ return;
+ }
+ if (obj._isVue) {
+ set(obj._data, key, val);
+ return;
+ }
+ var ob = obj.__ob__;
+ if (!ob) {
+ obj[key] = val;
+ return;
+ }
+ ob.convert(key, val);
+ ob.dep.notify();
+ if (ob.vms) {
+ var i = ob.vms.length;
+ while (i--) {
+ var vm = ob.vms[i];
+ vm._proxy(key);
+ vm._digest();
+ }
+ }
+ return val;
+ }
+
+ /**
+ * Delete a property and trigger change if necessary.
+ *
+ * @param {Object} obj
+ * @param {String} key
+ */
+
+ function del(obj, key) {
+ if (!hasOwn(obj, key)) {
+ return;
+ }
+ delete obj[key];
+ var ob = obj.__ob__;
+ if (!ob) {
+ if (obj._isVue) {
+ delete obj._data[key];
+ obj._digest();
+ }
+ return;
+ }
+ ob.dep.notify();
+ if (ob.vms) {
+ var i = ob.vms.length;
+ while (i--) {
+ var vm = ob.vms[i];
+ vm._unproxy(key);
+ vm._digest();
+ }
+ }
+ }
+
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
+ /**
+ * Check whether the object has the property.
+ *
+ * @param {Object} obj
+ * @param {String} key
+ * @return {Boolean}
+ */
+
+ function hasOwn(obj, key) {
+ return hasOwnProperty.call(obj, key);
+ }
+
+ /**
+ * Check if an expression is a literal value.
+ *
+ * @param {String} exp
+ * @return {Boolean}
+ */
+
+ var literalValueRE = /^\s?(true|false|-?[\d\.]+|'[^']*'|"[^"]*")\s?$/;
+
+ function isLiteral(exp) {
+ return literalValueRE.test(exp);
+ }
+
+ /**
+ * Check if a string starts with $ or _
+ *
+ * @param {String} str
+ * @return {Boolean}
+ */
+
+ function isReserved(str) {
+ var c = (str + '').charCodeAt(0);
+ return c === 0x24 || c === 0x5F;
+ }
+
+ /**
+ * Guard text output, make sure undefined outputs
+ * empty string
+ *
+ * @param {*} value
+ * @return {String}
+ */
+
+ function _toString(value) {
+ return value == null ? '' : value.toString();
+ }
+
+ /**
+ * Check and convert possible numeric strings to numbers
+ * before setting back to data
+ *
+ * @param {*} value
+ * @return {*|Number}
+ */
+
+ function toNumber(value) {
+ if (typeof value !== 'string') {
+ return value;
+ } else {
+ var parsed = Number(value);
+ return isNaN(parsed) ? value : parsed;
+ }
+ }
+
+ /**
+ * Convert string boolean literals into real booleans.
+ *
+ * @param {*} value
+ * @return {*|Boolean}
+ */
+
+ function toBoolean(value) {
+ return value === 'true' ? true : value === 'false' ? false : value;
+ }
+
+ /**
+ * Strip quotes from a string
+ *
+ * @param {String} str
+ * @return {String | false}
+ */
+
+ function stripQuotes(str) {
+ var a = str.charCodeAt(0);
+ var b = str.charCodeAt(str.length - 1);
+ return a === b && (a === 0x22 || a === 0x27) ? str.slice(1, -1) : str;
+ }
+
+ /**
+ * Camelize a hyphen-delmited string.
+ *
+ * @param {String} str
+ * @return {String}
+ */
+
+ var camelizeRE = /-(\w)/g;
+
+ function camelize(str) {
+ return str.replace(camelizeRE, toUpper);
+ }
+
+ function toUpper(_, c) {
+ return c ? c.toUpperCase() : '';
+ }
+
+ /**
+ * Hyphenate a camelCase string.
+ *
+ * @param {String} str
+ * @return {String}
+ */
+
+ var hyphenateRE = /([a-z\d])([A-Z])/g;
+
+ function hyphenate(str) {
+ return str.replace(hyphenateRE, '$1-$2').toLowerCase();
+ }
+
+ /**
+ * Converts hyphen/underscore/slash delimitered names into
+ * camelized classNames.
+ *
+ * e.g. my-component => MyComponent
+ * some_else => SomeElse
+ * some/comp => SomeComp
+ *
+ * @param {String} str
+ * @return {String}
+ */
+
+ var classifyRE = /(?:^|[-_\/])(\w)/g;
+
+ function classify(str) {
+ return str.replace(classifyRE, toUpper);
+ }
+
+ /**
+ * Simple bind, faster than native
+ *
+ * @param {Function} fn
+ * @param {Object} ctx
+ * @return {Function}
+ */
+
+ function bind(fn, ctx) {
+ return function (a) {
+ var l = arguments.length;
+ return l ? l > 1 ? fn.apply(ctx, arguments) : fn.call(ctx, a) : fn.call(ctx);
+ };
+ }
+
+ /**
+ * Convert an Array-like object to a real Array.
+ *
+ * @param {Array-like} list
+ * @param {Number} [start] - start index
+ * @return {Array}
+ */
+
+ function toArray(list, start) {
+ start = start || 0;
+ var i = list.length - start;
+ var ret = new Array(i);
+ while (i--) {
+ ret[i] = list[i + start];
+ }
+ return ret;
+ }
+
+ /**
+ * Mix properties into target object.
+ *
+ * @param {Object} to
+ * @param {Object} from
+ */
+
+ function extend(to, from) {
+ var keys = Object.keys(from);
+ var i = keys.length;
+ while (i--) {
+ to[keys[i]] = from[keys[i]];
+ }
+ return to;
+ }
+
+ /**
+ * Quick object check - this is primarily used to tell
+ * Objects from primitive values when we know the value
+ * is a JSON-compliant type.
+ *
+ * @param {*} obj
+ * @return {Boolean}
+ */
+
+ function isObject(obj) {
+ return obj !== null && typeof obj === 'object';
+ }
+
+ /**
+ * Strict object type check. Only returns true
+ * for plain JavaScript objects.
+ *
+ * @param {*} obj
+ * @return {Boolean}
+ */
+
+ var toString = Object.prototype.toString;
+ var OBJECT_STRING = '[object Object]';
+
+ function isPlainObject(obj) {
+ return toString.call(obj) === OBJECT_STRING;
+ }
+
+ /**
+ * Array type check.
+ *
+ * @param {*} obj
+ * @return {Boolean}
+ */
+
+ var isArray = Array.isArray;
+
+ /**
+ * Define a property.
+ *
+ * @param {Object} obj
+ * @param {String} key
+ * @param {*} val
+ * @param {Boolean} [enumerable]
+ */
+
+ function def(obj, key, val, enumerable) {
+ Object.defineProperty(obj, key, {
+ value: val,
+ enumerable: !!enumerable,
+ writable: true,
+ configurable: true
+ });
+ }
+
+ /**
+ * Debounce a function so it only gets called after the
+ * input stops arriving after the given wait period.
+ *
+ * @param {Function} func
+ * @param {Number} wait
+ * @return {Function} - the debounced function
+ */
+
+ function _debounce(func, wait) {
+ var timeout, args, context, timestamp, result;
+ var later = function later() {
+ var last = Date.now() - timestamp;
+ if (last < wait && last >= 0) {
+ timeout = setTimeout(later, wait - last);
+ } else {
+ timeout = null;
+ result = func.apply(context, args);
+ if (!timeout) context = args = null;
+ }
+ };
+ return function () {
+ context = this;
+ args = arguments;
+ timestamp = Date.now();
+ if (!timeout) {
+ timeout = setTimeout(later, wait);
+ }
+ return result;
+ };
+ }
+
+ /**
+ * Manual indexOf because it's slightly faster than
+ * native.
+ *
+ * @param {Array} arr
+ * @param {*} obj
+ */
+
+ function indexOf(arr, obj) {
+ var i = arr.length;
+ while (i--) {
+ if (arr[i] === obj) return i;
+ }
+ return -1;
+ }
+
+ /**
+ * Make a cancellable version of an async callback.
+ *
+ * @param {Function} fn
+ * @return {Function}
+ */
+
+ function cancellable(fn) {
+ var cb = function cb() {
+ if (!cb.cancelled) {
+ return fn.apply(this, arguments);
+ }
+ };
+ cb.cancel = function () {
+ cb.cancelled = true;
+ };
+ return cb;
+ }
+
+ /**
+ * Check if two values are loosely equal - that is,
+ * if they are plain objects, do they have the same shape?
+ *
+ * @param {*} a
+ * @param {*} b
+ * @return {Boolean}
+ */
+
+ function looseEqual(a, b) {
+ /* eslint-disable eqeqeq */
+ return a == b || (isObject(a) && isObject(b) ? JSON.stringify(a) === JSON.stringify(b) : false);
+ /* eslint-enable eqeqeq */
+ }
+
+ var hasProto = ('__proto__' in {});
+
+ // Browser environment sniffing
+ var inBrowser = typeof window !== 'undefined' && Object.prototype.toString.call(window) !== '[object Object]';
+
+ // detect devtools
+ var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
+
+ // UA sniffing for working around browser-specific quirks
+ var UA = inBrowser && window.navigator.userAgent.toLowerCase();
+ var isIE = UA && UA.indexOf('trident') > 0;
+ var isIE9 = UA && UA.indexOf('msie 9.0') > 0;
+ var isAndroid = UA && UA.indexOf('android') > 0;
+ var isIos = UA && /(iphone|ipad|ipod|ios)/i.test(UA);
+ var iosVersionMatch = isIos && UA.match(/os ([\d_]+)/);
+ var iosVersion = iosVersionMatch && iosVersionMatch[1].split('_');
+
+ // detecting iOS UIWebView by indexedDB
+ var hasMutationObserverBug = iosVersion && Number(iosVersion[0]) >= 9 && Number(iosVersion[1]) >= 3 && !window.indexedDB;
+
+ var transitionProp = undefined;
+ var transitionEndEvent = undefined;
+ var animationProp = undefined;
+ var animationEndEvent = undefined;
+
+ // Transition property/event sniffing
+ if (inBrowser && !isIE9) {
+ var isWebkitTrans = window.ontransitionend === undefined && window.onwebkittransitionend !== undefined;
+ var isWebkitAnim = window.onanimationend === undefined && window.onwebkitanimationend !== undefined;
+ transitionProp = isWebkitTrans ? 'WebkitTransition' : 'transition';
+ transitionEndEvent = isWebkitTrans ? 'webkitTransitionEnd' : 'transitionend';
+ animationProp = isWebkitAnim ? 'WebkitAnimation' : 'animation';
+ animationEndEvent = isWebkitAnim ? 'webkitAnimationEnd' : 'animationend';
+ }
+
+ /**
+ * Defer a task to execute it asynchronously. Ideally this
+ * should be executed as a microtask, so we leverage
+ * MutationObserver if it's available, and fallback to
+ * setTimeout(0).
+ *
+ * @param {Function} cb
+ * @param {Object} ctx
+ */
+
+ var nextTick = (function () {
+ var callbacks = [];
+ var pending = false;
+ var timerFunc;
+ function nextTickHandler() {
+ pending = false;
+ var copies = callbacks.slice(0);
+ callbacks = [];
+ for (var i = 0; i < copies.length; i++) {
+ copies[i]();
+ }
+ }
+
+ /* istanbul ignore if */
+ if (typeof MutationObserver !== 'undefined' && !hasMutationObserverBug) {
+ var counter = 1;
+ var observer = new MutationObserver(nextTickHandler);
+ var textNode = document.createTextNode(counter);
+ observer.observe(textNode, {
+ characterData: true
+ });
+ timerFunc = function () {
+ counter = (counter + 1) % 2;
+ textNode.data = counter;
+ };
+ } else {
+ // webpack attempts to inject a shim for setImmediate
+ // if it is used as a global, so we have to work around that to
+ // avoid bundling unnecessary code.
+ var context = inBrowser ? window : typeof global !== 'undefined' ? global : {};
+ timerFunc = context.setImmediate || setTimeout;
+ }
+ return function (cb, ctx) {
+ var func = ctx ? function () {
+ cb.call(ctx);
+ } : cb;
+ callbacks.push(func);
+ if (pending) return;
+ pending = true;
+ timerFunc(nextTickHandler, 0);
+ };
+ })();
+
+ var _Set = undefined;
+ /* istanbul ignore if */
+ if (typeof Set !== 'undefined' && Set.toString().match(/native code/)) {
+ // use native Set when available.
+ _Set = Set;
+ } else {
+ // a non-standard Set polyfill that only works with primitive keys.
+ _Set = function () {
+ this.set = Object.create(null);
+ };
+ _Set.prototype.has = function (key) {
+ return this.set[key] !== undefined;
+ };
+ _Set.prototype.add = function (key) {
+ this.set[key] = 1;
+ };
+ _Set.prototype.clear = function () {
+ this.set = Object.create(null);
+ };
+ }
+
+ function Cache(limit) {
+ this.size = 0;
+ this.limit = limit;
+ this.head = this.tail = undefined;
+ this._keymap = Object.create(null);
+ }
+
+ var p = Cache.prototype;
+
+ /**
+ * Put <value> into the cache associated with <key>.
+ * Returns the entry which was removed to make room for
+ * the new entry. Otherwise undefined is returned.
+ * (i.e. if there was enough room already).
+ *
+ * @param {String} key
+ * @param {*} value
+ * @return {Entry|undefined}
+ */
+
+ p.put = function (key, value) {
+ var removed;
+
+ var entry = this.get(key, true);
+ if (!entry) {
+ if (this.size === this.limit) {
+ removed = this.shift();
+ }
+ entry = {
+ key: key
+ };
+ this._keymap[key] = entry;
+ if (this.tail) {
+ this.tail.newer = entry;
+ entry.older = this.tail;
+ } else {
+ this.head = entry;
+ }
+ this.tail = entry;
+ this.size++;
+ }
+ entry.value = value;
+
+ return removed;
+ };
+
+ /**
+ * Purge the least recently used (oldest) entry from the
+ * cache. Returns the removed entry or undefined if the
+ * cache was empty.
+ */
+
+ p.shift = function () {
+ var entry = this.head;
+ if (entry) {
+ this.head = this.head.newer;
+ this.head.older = undefined;
+ entry.newer = entry.older = undefined;
+ this._keymap[entry.key] = undefined;
+ this.size--;
+ }
+ return entry;
+ };
+
+ /**
+ * Get and register recent use of <key>. Returns the value
+ * associated with <key> or undefined if not in cache.
+ *
+ * @param {String} key
+ * @param {Boolean} returnEntry
+ * @return {Entry|*}
+ */
+
+ p.get = function (key, returnEntry) {
+ var entry = this._keymap[key];
+ if (entry === undefined) return;
+ if (entry === this.tail) {
+ return returnEntry ? entry : entry.value;
+ }
+ // HEAD--------------TAIL
+ // <.older .newer>
+ // <--- add direction --
+ // A B C <D> E
+ if (entry.newer) {
+ if (entry === this.head) {
+ this.head = entry.newer;
+ }
+ entry.newer.older = entry.older; // C <-- E.
+ }
+ if (entry.older) {
+ entry.older.newer = entry.newer; // C. --> E
+ }
+ entry.newer = undefined; // D --x
+ entry.older = this.tail; // D. --> E
+ if (this.tail) {
+ this.tail.newer = entry; // E. <-- D
+ }
+ this.tail = entry;
+ return returnEntry ? entry : entry.value;
+ };
+
+ var cache$1 = new Cache(1000);
+ var filterTokenRE = /[^\s'"]+|'[^']*'|"[^"]*"/g;
+ var reservedArgRE = /^in$|^-?\d+/;
+
+ /**
+ * Parser state
+ */
+
+ var str;
+ var dir;
+ var c;
+ var prev;
+ var i;
+ var l;
+ var lastFilterIndex;
+ var inSingle;
+ var inDouble;
+ var curly;
+ var square;
+ var paren;
+ /**
+ * Push a filter to the current directive object
+ */
+
+ function pushFilter() {
+ var exp = str.slice(lastFilterIndex, i).trim();
+ var filter;
+ if (exp) {
+ filter = {};
+ var tokens = exp.match(filterTokenRE);
+ filter.name = tokens[0];
+ if (tokens.length > 1) {
+ filter.args = tokens.slice(1).map(processFilterArg);
+ }
+ }
+ if (filter) {
+ (dir.filters = dir.filters || []).push(filter);
+ }
+ lastFilterIndex = i + 1;
+ }
+
+ /**
+ * Check if an argument is dynamic and strip quotes.
+ *
+ * @param {String} arg
+ * @return {Object}
+ */
+
+ function processFilterArg(arg) {
+ if (reservedArgRE.test(arg)) {
+ return {
+ value: toNumber(arg),
+ dynamic: false
+ };
+ } else {
+ var stripped = stripQuotes(arg);
+ var dynamic = stripped === arg;
+ return {
+ value: dynamic ? arg : stripped,
+ dynamic: dynamic
+ };
+ }
+ }
+
+ /**
+ * Parse a directive value and extract the expression
+ * and its filters into a descriptor.
+ *
+ * Example:
+ *
+ * "a + 1 | uppercase" will yield:
+ * {
+ * expression: 'a + 1',
+ * filters: [
+ * { name: 'uppercase', args: null }
+ * ]
+ * }
+ *
+ * @param {String} s
+ * @return {Object}
+ */
+
+ function parseDirective(s) {
+ var hit = cache$1.get(s);
+ if (hit) {
+ return hit;
+ }
+
+ // reset parser state
+ str = s;
+ inSingle = inDouble = false;
+ curly = square = paren = 0;
+ lastFilterIndex = 0;
+ dir = {};
+
+ for (i = 0, l = str.length; i < l; i++) {
+ prev = c;
+ c = str.charCodeAt(i);
+ if (inSingle) {
+ // check single quote
+ if (c === 0x27 && prev !== 0x5C) inSingle = !inSingle;
+ } else if (inDouble) {
+ // check double quote
+ if (c === 0x22 && prev !== 0x5C) inDouble = !inDouble;
+ } else if (c === 0x7C && // pipe
+ str.charCodeAt(i + 1) !== 0x7C && str.charCodeAt(i - 1) !== 0x7C) {
+ if (dir.expression == null) {
+ // first filter, end of expression
+ lastFilterIndex = i + 1;
+ dir.expression = str.slice(0, i).trim();
+ } else {
+ // already has filter
+ pushFilter();
+ }
+ } else {
+ switch (c) {
+ case 0x22:
+ inDouble = true;break; // "
+ case 0x27:
+ inSingle = true;break; // '
+ case 0x28:
+ paren++;break; // (
+ case 0x29:
+ paren--;break; // )
+ case 0x5B:
+ square++;break; // [
+ case 0x5D:
+ square--;break; // ]
+ case 0x7B:
+ curly++;break; // {
+ case 0x7D:
+ curly--;break; // }
+ }
+ }
+ }
+
+ if (dir.expression == null) {
+ dir.expression = str.slice(0, i).trim();
+ } else if (lastFilterIndex !== 0) {
+ pushFilter();
+ }
+
+ cache$1.put(s, dir);
+ return dir;
+ }
+
+var directive = Object.freeze({
+ parseDirective: parseDirective
+ });
+
+ var regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g;
+ var cache = undefined;
+ var tagRE = undefined;
+ var htmlRE = undefined;
+ /**
+ * Escape a string so it can be used in a RegExp
+ * constructor.
+ *
+ * @param {String} str
+ */
+
+ function escapeRegex(str) {
+ return str.replace(regexEscapeRE, '\\$&');
+ }
+
+ function compileRegex() {
+ var open = escapeRegex(config.delimiters[0]);
+ var close = escapeRegex(config.delimiters[1]);
+ var unsafeOpen = escapeRegex(config.unsafeDelimiters[0]);
+ var unsafeClose = escapeRegex(config.unsafeDelimiters[1]);
+ tagRE = new RegExp(unsafeOpen + '((?:.|\\n)+?)' + unsafeClose + '|' + open + '((?:.|\\n)+?)' + close, 'g');
+ htmlRE = new RegExp('^' + unsafeOpen + '((?:.|\\n)+?)' + unsafeClose + '$');
+ // reset cache
+ cache = new Cache(1000);
+ }
+
+ /**
+ * Parse a template text string into an array of tokens.
+ *
+ * @param {String} text
+ * @return {Array<Object> | null}
+ * - {String} type
+ * - {String} value
+ * - {Boolean} [html]
+ * - {Boolean} [oneTime]
+ */
+
+ function parseText(text) {
+ if (!cache) {
+ compileRegex();
+ }
+ var hit = cache.get(text);
+ if (hit) {
+ return hit;
+ }
+ if (!tagRE.test(text)) {
+ return null;
+ }
+ var tokens = [];
+ var lastIndex = tagRE.lastIndex = 0;
+ var match, index, html, value, first, oneTime;
+ /* eslint-disable no-cond-assign */
+ while (match = tagRE.exec(text)) {
+ /* eslint-enable no-cond-assign */
+ index = match.index;
+ // push text token
+ if (index > lastIndex) {
+ tokens.push({
+ value: text.slice(lastIndex, index)
+ });
+ }
+ // tag token
+ html = htmlRE.test(match[0]);
+ value = html ? match[1] : match[2];
+ first = value.charCodeAt(0);
+ oneTime = first === 42; // *
+ value = oneTime ? value.slice(1) : value;
+ tokens.push({
+ tag: true,
+ value: value.trim(),
+ html: html,
+ oneTime: oneTime
+ });
+ lastIndex = index + match[0].length;
+ }
+ if (lastIndex < text.length) {
+ tokens.push({
+ value: text.slice(lastIndex)
+ });
+ }
+ cache.put(text, tokens);
+ return tokens;
+ }
+
+ /**
+ * Format a list of tokens into an expression.
+ * e.g. tokens parsed from 'a {{b}} c' can be serialized
+ * into one single expression as '"a " + b + " c"'.
+ *
+ * @param {Array} tokens
+ * @param {Vue} [vm]
+ * @return {String}
+ */
+
+ function tokensToExp(tokens, vm) {
+ if (tokens.length > 1) {
+ return tokens.map(function (token) {
+ return formatToken(token, vm);
+ }).join('+');
+ } else {
+ return formatToken(tokens[0], vm, true);
+ }
+ }
+
+ /**
+ * Format a single token.
+ *
+ * @param {Object} token
+ * @param {Vue} [vm]
+ * @param {Boolean} [single]
+ * @return {String}
+ */
+
+ function formatToken(token, vm, single) {
+ return token.tag ? token.oneTime && vm ? '"' + vm.$eval(token.value) + '"' : inlineFilters(token.value, single) : '"' + token.value + '"';
+ }
+
+ /**
+ * For an attribute with multiple interpolation tags,
+ * e.g. attr="some-{{thing | filter}}", in order to combine
+ * the whole thing into a single watchable expression, we
+ * have to inline those filters. This function does exactly
+ * that. This is a bit hacky but it avoids heavy changes
+ * to directive parser and watcher mechanism.
+ *
+ * @param {String} exp
+ * @param {Boolean} single
+ * @return {String}
+ */
+
+ var filterRE = /[^|]\|[^|]/;
+ function inlineFilters(exp, single) {
+ if (!filterRE.test(exp)) {
+ return single ? exp : '(' + exp + ')';
+ } else {
+ var dir = parseDirective(exp);
+ if (!dir.filters) {
+ return '(' + exp + ')';
+ } else {
+ return 'this._applyFilters(' + dir.expression + // value
+ ',null,' + // oldValue (null for read)
+ JSON.stringify(dir.filters) + // filter descriptors
+ ',false)'; // write?
+ }
+ }
+ }
+
+var text = Object.freeze({
+ compileRegex: compileRegex,
+ parseText: parseText,
+ tokensToExp: tokensToExp
+ });
+
+ var delimiters = ['{{', '}}'];
+ var unsafeDelimiters = ['{{{', '}}}'];
+
+ var config = Object.defineProperties({
+
+ /**
+ * Whether to print debug messages.
+ * Also enables stack trace for warnings.
+ *
+ * @type {Boolean}
+ */
+
+ debug: false,
+
+ /**
+ * Whether to suppress warnings.
+ *
+ * @type {Boolean}
+ */
+
+ silent: false,
+
+ /**
+ * Whether to use async rendering.
+ */
+
+ async: true,
+
+ /**
+ * Whether to warn against errors caught when evaluating
+ * expressions.
+ */
+
+ warnExpressionErrors: true,
+
+ /**
+ * Whether to allow devtools inspection.
+ * Disabled by default in production builds.
+ */
+
+ devtools: 'development' !== 'production',
+
+ /**
+ * Internal flag to indicate the delimiters have been
+ * changed.
+ *
+ * @type {Boolean}
+ */
+
+ _delimitersChanged: true,
+
+ /**
+ * List of asset types that a component can own.
+ *
+ * @type {Array}
+ */
+
+ _assetTypes: ['component', 'directive', 'elementDirective', 'filter', 'transition', 'partial'],
+
+ /**
+ * prop binding modes
+ */
+
+ _propBindingModes: {
+ ONE_WAY: 0,
+ TWO_WAY: 1,
+ ONE_TIME: 2
+ },
+
+ /**
+ * Max circular updates allowed in a batcher flush cycle.
+ */
+
+ _maxUpdateCount: 100
+
+ }, {
+ delimiters: { /**
+ * Interpolation delimiters. Changing these would trigger
+ * the text parser to re-compile the regular expressions.
+ *
+ * @type {Array<String>}
+ */
+
+ get: function get() {
+ return delimiters;
+ },
+ set: function set(val) {
+ delimiters = val;
+ compileRegex();
+ },
+ configurable: true,
+ enumerable: true
+ },
+ unsafeDelimiters: {
+ get: function get() {
+ return unsafeDelimiters;
+ },
+ set: function set(val) {
+ unsafeDelimiters = val;
+ compileRegex();
+ },
+ configurable: true,
+ enumerable: true
+ }
+ });
+
+ var warn = undefined;
+ var formatComponentName = undefined;
+
+ if ('development' !== 'production') {
+ (function () {
+ var hasConsole = typeof console !== 'undefined';
+
+ warn = function (msg, vm) {
+ if (hasConsole && !config.silent) {
+ console.error('[Vue warn]: ' + msg + (vm ? formatComponentName(vm) : ''));
+ }
+ };
+
+ formatComponentName = function (vm) {
+ var name = vm._isVue ? vm.$options.name : vm.name;
+ return name ? ' (found in component: <' + hyphenate(name) + '>)' : '';
+ };
+ })();
+ }
+
+ /**
+ * Append with transition.
+ *
+ * @param {Element} el
+ * @param {Element} target
+ * @param {Vue} vm
+ * @param {Function} [cb]
+ */
+
+ function appendWithTransition(el, target, vm, cb) {
+ applyTransition(el, 1, function () {
+ target.appendChild(el);
+ }, vm, cb);
+ }
+
+ /**
+ * InsertBefore with transition.
+ *
+ * @param {Element} el
+ * @param {Element} target
+ * @param {Vue} vm
+ * @param {Function} [cb]
+ */
+
+ function beforeWithTransition(el, target, vm, cb) {
+ applyTransition(el, 1, function () {
+ before(el, target);
+ }, vm, cb);
+ }
+
+ /**
+ * Remove with transition.
+ *
+ * @param {Element} el
+ * @param {Vue} vm
+ * @param {Function} [cb]
+ */
+
+ function removeWithTransition(el, vm, cb) {
+ applyTransition(el, -1, function () {
+ remove(el);
+ }, vm, cb);
+ }
+
+ /**
+ * Apply transitions with an operation callback.
+ *
+ * @param {Element} el
+ * @param {Number} direction
+ * 1: enter
+ * -1: leave
+ * @param {Function} op - the actual DOM operation
+ * @param {Vue} vm
+ * @param {Function} [cb]
+ */
+
+ function applyTransition(el, direction, op, vm, cb) {
+ var transition = el.__v_trans;
+ if (!transition ||
+ // skip if there are no js hooks and CSS transition is
+ // not supported
+ !transition.hooks && !transitionEndEvent ||
+ // skip transitions for initial compile
+ !vm._isCompiled ||
+ // if the vm is being manipulated by a parent directive
+ // during the parent's compilation phase, skip the
+ // animation.
+ vm.$parent && !vm.$parent._isCompiled) {
+ op();
+ if (cb) cb();
+ return;
+ }
+ var action = direction > 0 ? 'enter' : 'leave';
+ transition[action](op, cb);
+ }
+
+var transition = Object.freeze({
+ appendWithTransition: appendWithTransition,
+ beforeWithTransition: beforeWithTransition,
+ removeWithTransition: removeWithTransition,
+ applyTransition: applyTransition
+ });
+
+ /**
+ * Query an element selector if it's not an element already.
+ *
+ * @param {String|Element} el
+ * @return {Element}
+ */
+
+ function query(el) {
+ if (typeof el === 'string') {
+ var selector = el;
+ el = document.querySelector(el);
+ if (!el) {
+ 'development' !== 'production' && warn('Cannot find element: ' + selector);
+ }
+ }
+ return el;
+ }
+
+ /**
+ * Check if a node is in the document.
+ * Note: document.documentElement.contains should work here
+ * but always returns false for comment nodes in phantomjs,
+ * making unit tests difficult. This is fixed by doing the
+ * contains() check on the node's parentNode instead of
+ * the node itself.
+ *
+ * @param {Node} node
+ * @return {Boolean}
+ */
+
+ function inDoc(node) {
+ if (!node) return false;
+ var doc = node.ownerDocument.documentElement;
+ var parent = node.parentNode;
+ return doc === node || doc === parent || !!(parent && parent.nodeType === 1 && doc.contains(parent));
+ }
+
+ /**
+ * Get and remove an attribute from a node.
+ *
+ * @param {Node} node
+ * @param {String} _attr
+ */
+
+ function getAttr(node, _attr) {
+ var val = node.getAttribute(_attr);
+ if (val !== null) {
+ node.removeAttribute(_attr);
+ }
+ return val;
+ }
+
+ /**
+ * Get an attribute with colon or v-bind: prefix.
+ *
+ * @param {Node} node
+ * @param {String} name
+ * @return {String|null}
+ */
+
+ function getBindAttr(node, name) {
+ var val = getAttr(node, ':' + name);
+ if (val === null) {
+ val = getAttr(node, 'v-bind:' + name);
+ }
+ return val;
+ }
+
+ /**
+ * Check the presence of a bind attribute.
+ *
+ * @param {Node} node
+ * @param {String} name
+ * @return {Boolean}
+ */
+
+ function hasBindAttr(node, name) {
+ return node.hasAttribute(name) || node.hasAttribute(':' + name) || node.hasAttribute('v-bind:' + name);
+ }
+
+ /**
+ * Insert el before target
+ *
+ * @param {Element} el
+ * @param {Element} target
+ */
+
+ function before(el, target) {
+ target.parentNode.insertBefore(el, target);
+ }
+
+ /**
+ * Insert el after target
+ *
+ * @param {Element} el
+ * @param {Element} target
+ */
+
+ function after(el, target) {
+ if (target.nextSibling) {
+ before(el, target.nextSibling);
+ } else {
+ target.parentNode.appendChild(el);
+ }
+ }
+
+ /**
+ * Remove el from DOM
+ *
+ * @param {Element} el
+ */
+
+ function remove(el) {
+ el.parentNode.removeChild(el);
+ }
+
+ /**
+ * Prepend el to target
+ *
+ * @param {Element} el
+ * @param {Element} target
+ */
+
+ function prepend(el, target) {
+ if (target.firstChild) {
+ before(el, target.firstChild);
+ } else {
+ target.appendChild(el);
+ }
+ }
+
+ /**
+ * Replace target with el
+ *
+ * @param {Element} target
+ * @param {Element} el
+ */
+
+ function replace(target, el) {
+ var parent = target.parentNode;
+ if (parent) {
+ parent.replaceChild(el, target);
+ }
+ }
+
+ /**
+ * Add event listener shorthand.
+ *
+ * @param {Element} el
+ * @param {String} event
+ * @param {Function} cb
+ * @param {Boolean} [useCapture]
+ */
+
+ function on(el, event, cb, useCapture) {
+ el.addEventListener(event, cb, useCapture);
+ }
+
+ /**
+ * Remove event listener shorthand.
+ *
+ * @param {Element} el
+ * @param {String} event
+ * @param {Function} cb
+ */
+
+ function off(el, event, cb) {
+ el.removeEventListener(event, cb);
+ }
+
+ /**
+ * For IE9 compat: when both class and :class are present
+ * getAttribute('class') returns wrong value...
+ *
+ * @param {Element} el
+ * @return {String}
+ */
+
+ function getClass(el) {
+ var classname = el.className;
+ if (typeof classname === 'object') {
+ classname = classname.baseVal || '';
+ }
+ return classname;
+ }
+
+ /**
+ * In IE9, setAttribute('class') will result in empty class
+ * if the element also has the :class attribute; However in
+ * PhantomJS, setting `className` does not work on SVG elements...
+ * So we have to do a conditional check here.
+ *
+ * @param {Element} el
+ * @param {String} cls
+ */
+
+ function setClass(el, cls) {
+ /* istanbul ignore if */
+ if (isIE9 && !/svg$/.test(el.namespaceURI)) {
+ el.className = cls;
+ } else {
+ el.setAttribute('class', cls);
+ }
+ }
+
+ /**
+ * Add class with compatibility for IE & SVG
+ *
+ * @param {Element} el
+ * @param {String} cls
+ */
+
+ function addClass(el, cls) {
+ if (el.classList) {
+ el.classList.add(cls);
+ } else {
+ var cur = ' ' + getClass(el) + ' ';
+ if (cur.indexOf(' ' + cls + ' ') < 0) {
+ setClass(el, (cur + cls).trim());
+ }
+ }
+ }
+
+ /**
+ * Remove class with compatibility for IE & SVG
+ *
+ * @param {Element} el
+ * @param {String} cls
+ */
+
+ function removeClass(el, cls) {
+ if (el.classList) {
+ el.classList.remove(cls);
+ } else {
+ var cur = ' ' + getClass(el) + ' ';
+ var tar = ' ' + cls + ' ';
+ while (cur.indexOf(tar) >= 0) {
+ cur = cur.replace(tar, ' ');
+ }
+ setClass(el, cur.trim());
+ }
+ if (!el.className) {
+ el.removeAttribute('class');
+ }
+ }
+
+ /**
+ * Extract raw content inside an element into a temporary
+ * container div
+ *
+ * @param {Element} el
+ * @param {Boolean} asFragment
+ * @return {Element|DocumentFragment}
+ */
+
+ function extractContent(el, asFragment) {
+ var child;
+ var rawContent;
+ /* istanbul ignore if */
+ if (isTemplate(el) && isFragment(el.content)) {
+ el = el.content;
+ }
+ if (el.hasChildNodes()) {
+ trimNode(el);
+ rawContent = asFragment ? document.createDocumentFragment() : document.createElement('div');
+ /* eslint-disable no-cond-assign */
+ while (child = el.firstChild) {
+ /* eslint-enable no-cond-assign */
+ rawContent.appendChild(child);
+ }
+ }
+ return rawContent;
+ }
+
+ /**
+ * Trim possible empty head/tail text and comment
+ * nodes inside a parent.
+ *
+ * @param {Node} node
+ */
+
+ function trimNode(node) {
+ var child;
+ /* eslint-disable no-sequences */
+ while ((child = node.firstChild, isTrimmable(child))) {
+ node.removeChild(child);
+ }
+ while ((child = node.lastChild, isTrimmable(child))) {
+ node.removeChild(child);
+ }
+ /* eslint-enable no-sequences */
+ }
+
+ function isTrimmable(node) {
+ return node && (node.nodeType === 3 && !node.data.trim() || node.nodeType === 8);
+ }
+
+ /**
+ * Check if an element is a template tag.
+ * Note if the template appears inside an SVG its tagName
+ * will be in lowercase.
+ *
+ * @param {Element} el
+ */
+
+ function isTemplate(el) {
+ return el.tagName && el.tagName.toLowerCase() === 'template';
+ }
+
+ /**
+ * Create an "anchor" for performing dom insertion/removals.
+ * This is used in a number of scenarios:
+ * - fragment instance
+ * - v-html
+ * - v-if
+ * - v-for
+ * - component
+ *
+ * @param {String} content
+ * @param {Boolean} persist - IE trashes empty textNodes on
+ * cloneNode(true), so in certain
+ * cases the anchor needs to be
+ * non-empty to be persisted in
+ * templates.
+ * @return {Comment|Text}
+ */
+
+ function createAnchor(content, persist) {
+ var anchor = config.debug ? document.createComment(content) : document.createTextNode(persist ? ' ' : '');
+ anchor.__v_anchor = true;
+ return anchor;
+ }
+
+ /**
+ * Find a component ref attribute that starts with $.
+ *
+ * @param {Element} node
+ * @return {String|undefined}
+ */
+
+ var refRE = /^v-ref:/;
+
+ function findRef(node) {
+ if (node.hasAttributes()) {
+ var attrs = node.attributes;
+ for (var i = 0, l = attrs.length; i < l; i++) {
+ var name = attrs[i].name;
+ if (refRE.test(name)) {
+ return camelize(name.replace(refRE, ''));
+ }
+ }
+ }
+ }
+
+ /**
+ * Map a function to a range of nodes .
+ *
+ * @param {Node} node
+ * @param {Node} end
+ * @param {Function} op
+ */
+
+ function mapNodeRange(node, end, op) {
+ var next;
+ while (node !== end) {
+ next = node.nextSibling;
+ op(node);
+ node = next;
+ }
+ op(end);
+ }
+
+ /**
+ * Remove a range of nodes with transition, store
+ * the nodes in a fragment with correct ordering,
+ * and call callback when done.
+ *
+ * @param {Node} start
+ * @param {Node} end
+ * @param {Vue} vm
+ * @param {DocumentFragment} frag
+ * @param {Function} cb
+ */
+
+ function removeNodeRange(start, end, vm, frag, cb) {
+ var done = false;
+ var removed = 0;
+ var nodes = [];
+ mapNodeRange(start, end, function (node) {
+ if (node === end) done = true;
+ nodes.push(node);
+ removeWithTransition(node, vm, onRemoved);
+ });
+ function onRemoved() {
+ removed++;
+ if (done && removed >= nodes.length) {
+ for (var i = 0; i < nodes.length; i++) {
+ frag.appendChild(nodes[i]);
+ }
+ cb && cb();
+ }
+ }
+ }
+
+ /**
+ * Check if a node is a DocumentFragment.
+ *
+ * @param {Node} node
+ * @return {Boolean}
+ */
+
+ function isFragment(node) {
+ return node && node.nodeType === 11;
+ }
+
+ /**
+ * Get outerHTML of elements, taking care
+ * of SVG elements in IE as well.
+ *
+ * @param {Element} el
+ * @return {String}
+ */
+
+ function getOuterHTML(el) {
+ if (el.outerHTML) {
+ return el.outerHTML;
+ } else {
+ var container = document.createElement('div');
+ container.appendChild(el.cloneNode(true));
+ return container.innerHTML;
+ }
+ }
+
+ var commonTagRE = /^(div|p|span|img|a|b|i|br|ul|ol|li|h1|h2|h3|h4|h5|h6|code|pre|table|th|td|tr|form|label|input|select|option|nav|article|section|header|footer)$/i;
+ var reservedTagRE = /^(slot|partial|component)$/i;
+
+ var isUnknownElement = undefined;
+ if ('development' !== 'production') {
+ isUnknownElement = function (el, tag) {
+ if (tag.indexOf('-') > -1) {
+ // http://stackoverflow.com/a/28210364/1070244
+ return el.constructor === window.HTMLUnknownElement || el.constructor === window.HTMLElement;
+ } else {
+ return (/HTMLUnknownElement/.test(el.toString()) &&
+ // Chrome returns unknown for several HTML5 elements.
+ // https://code.google.com/p/chromium/issues/detail?id=540526
+ // Firefox returns unknown for some "Interactive elements."
+ !/^(data|time|rtc|rb|details|dialog|summary)$/.test(tag)
+ );
+ }
+ };
+ }
+
+ /**
+ * Check if an element is a component, if yes return its
+ * component id.
+ *
+ * @param {Element} el
+ * @param {Object} options
+ * @return {Object|undefined}
+ */
+
+ function checkComponentAttr(el, options) {
+ var tag = el.tagName.toLowerCase();
+ var hasAttrs = el.hasAttributes();
+ if (!commonTagRE.test(tag) && !reservedTagRE.test(tag)) {
+ if (resolveAsset(options, 'components', tag)) {
+ return { id: tag };
+ } else {
+ var is = hasAttrs && getIsBinding(el, options);
+ if (is) {
+ return is;
+ } else if ('development' !== 'production') {
+ var expectedTag = options._componentNameMap && options._componentNameMap[tag];
+ if (expectedTag) {
+ warn('Unknown custom element: <' + tag + '> - ' + 'did you mean <' + expectedTag + '>? ' + 'HTML is case-insensitive, remember to use kebab-case in templates.');
+ } else if (isUnknownElement(el, tag)) {
+ warn('Unknown custom element: <' + tag + '> - did you ' + 'register the component correctly? For recursive components, ' + 'make sure to provide the "name" option.');
+ }
+ }
+ }
+ } else if (hasAttrs) {
+ return getIsBinding(el, options);
+ }
+ }
+
+ /**
+ * Get "is" binding from an element.
+ *
+ * @param {Element} el
+ * @param {Object} options
+ * @return {Object|undefined}
+ */
+
+ function getIsBinding(el, options) {
+ // dynamic syntax
+ var exp = el.getAttribute('is');
+ if (exp != null) {
+ if (resolveAsset(options, 'components', exp)) {
+ el.removeAttribute('is');
+ return { id: exp };
+ }
+ } else {
+ exp = getBindAttr(el, 'is');
+ if (exp != null) {
+ return { id: exp, dynamic: true };
+ }
+ }
+ }
+
+ /**
+ * Option overwriting strategies are functions that handle
+ * how to merge a parent option value and a child option
+ * value into the final value.
+ *
+ * All strategy functions follow the same signature:
+ *
+ * @param {*} parentVal
+ * @param {*} childVal
+ * @param {Vue} [vm]
+ */
+
+ var strats = config.optionMergeStrategies = Object.create(null);
+
+ /**
+ * Helper that recursively merges two data objects together.
+ */
+
+ function mergeData(to, from) {
+ var key, toVal, fromVal;
+ for (key in from) {
+ toVal = to[key];
+ fromVal = from[key];
+ if (!hasOwn(to, key)) {
+ set(to, key, fromVal);
+ } else if (isObject(toVal) && isObject(fromVal)) {
+ mergeData(toVal, fromVal);
+ }
+ }
+ return to;
+ }
+
+ /**
+ * Data
+ */
+
+ strats.data = function (parentVal, childVal, vm) {
+ if (!vm) {
+ // in a Vue.extend merge, both should be functions
+ if (!childVal) {
+ return parentVal;
+ }
+ if (typeof childVal !== 'function') {
+ 'development' !== 'production' && warn('The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm);
+ return parentVal;
+ }
+ if (!parentVal) {
+ return childVal;
+ }
+ // when parentVal & childVal are both present,
+ // we need to return a function that returns the
+ // merged result of both functions... no need to
+ // check if parentVal is a function here because
+ // it has to be a function to pass previous merges.
+ return function mergedDataFn() {
+ return mergeData(childVal.call(this), parentVal.call(this));
+ };
+ } else if (parentVal || childVal) {
+ return function mergedInstanceDataFn() {
+ // instance merge
+ var instanceData = typeof childVal === 'function' ? childVal.call(vm) : childVal;
+ var defaultData = typeof parentVal === 'function' ? parentVal.call(vm) : undefined;
+ if (instanceData) {
+ return mergeData(instanceData, defaultData);
+ } else {
+ return defaultData;
+ }
+ };
+ }
+ };
+
+ /**
+ * El
+ */
+
+ strats.el = function (parentVal, childVal, vm) {
+ if (!vm && childVal && typeof childVal !== 'function') {
+ 'development' !== 'production' && warn('The "el" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm);
+ return;
+ }
+ var ret = childVal || parentVal;
+ // invoke the element factory if this is instance merge
+ return vm && typeof ret === 'function' ? ret.call(vm) : ret;
+ };
+
+ /**
+ * Hooks and param attributes are merged as arrays.
+ */
+
+ strats.init = strats.created = strats.ready = strats.attached = strats.detached = strats.beforeCompile = strats.compiled = strats.beforeDestroy = strats.destroyed = strats.activate = function (parentVal, childVal) {
+ return childVal ? parentVal ? parentVal.concat(childVal) : isArray(childVal) ? childVal : [childVal] : parentVal;
+ };
+
+ /**
+ * Assets
+ *
+ * When a vm is present (instance creation), we need to do
+ * a three-way merge between constructor options, instance
+ * options and parent options.
+ */
+
+ function mergeAssets(parentVal, childVal) {
+ var res = Object.create(parentVal || null);
+ return childVal ? extend(res, guardArrayAssets(childVal)) : res;
+ }
+
+ config._assetTypes.forEach(function (type) {
+ strats[type + 's'] = mergeAssets;
+ });
+
+ /**
+ * Events & Watchers.
+ *
+ * Events & watchers hashes should not overwrite one
+ * another, so we merge them as arrays.
+ */
+
+ strats.watch = strats.events = function (parentVal, childVal) {
+ if (!childVal) return parentVal;
+ if (!parentVal) return childVal;
+ var ret = {};
+ extend(ret, parentVal);
+ for (var key in childVal) {
+ var parent = ret[key];
+ var child = childVal[key];
+ if (parent && !isArray(parent)) {
+ parent = [parent];
+ }
+ ret[key] = parent ? parent.concat(child) : [child];
+ }
+ return ret;
+ };
+
+ /**
+ * Other object hashes.
+ */
+
+ strats.props = strats.methods = strats.computed = function (parentVal, childVal) {
+ if (!childVal) return parentVal;
+ if (!parentVal) return childVal;
+ var ret = Object.create(null);
+ extend(ret, parentVal);
+ extend(ret, childVal);
+ return ret;
+ };
+
+ /**
+ * Default strategy.
+ */
+
+ var defaultStrat = function defaultStrat(parentVal, childVal) {
+ return childVal === undefined ? parentVal : childVal;
+ };
+
+ /**
+ * Make sure component options get converted to actual
+ * constructors.
+ *
+ * @param {Object} options
+ */
+
+ function guardComponents(options) {
+ if (options.components) {
+ var components = options.components = guardArrayAssets(options.components);
+ var ids = Object.keys(components);
+ var def;
+ if ('development' !== 'production') {
+ var map = options._componentNameMap = {};
+ }
+ for (var i = 0, l = ids.length; i < l; i++) {
+ var key = ids[i];
+ if (commonTagRE.test(key) || reservedTagRE.test(key)) {
+ 'development' !== 'production' && warn('Do not use built-in or reserved HTML elements as component ' + 'id: ' + key);
+ continue;
+ }
+ // record a all lowercase <-> kebab-case mapping for
+ // possible custom element case error warning
+ if ('development' !== 'production') {
+ map[key.replace(/-/g, '').toLowerCase()] = hyphenate(key);
+ }
+ def = components[key];
+ if (isPlainObject(def)) {
+ components[key] = Vue.extend(def);
+ }
+ }
+ }
+ }
+
+ /**
+ * Ensure all props option syntax are normalized into the
+ * Object-based format.
+ *
+ * @param {Object} options
+ */
+
+ function guardProps(options) {
+ var props = options.props;
+ var i, val;
+ if (isArray(props)) {
+ options.props = {};
+ i = props.length;
+ while (i--) {
+ val = props[i];
+ if (typeof val === 'string') {
+ options.props[val] = null;
+ } else if (val.name) {
+ options.props[val.name] = val;
+ }
+ }
+ } else if (isPlainObject(props)) {
+ var keys = Object.keys(props);
+ i = keys.length;
+ while (i--) {
+ val = props[keys[i]];
+ if (typeof val === 'function') {
+ props[keys[i]] = { type: val };
+ }
+ }
+ }
+ }
+
+ /**
+ * Guard an Array-format assets option and converted it
+ * into the key-value Object format.
+ *
+ * @param {Object|Array} assets
+ * @return {Object}
+ */
+
+ function guardArrayAssets(assets) {
+ if (isArray(assets)) {
+ var res = {};
+ var i = assets.length;
+ var asset;
+ while (i--) {
+ asset = assets[i];
+ var id = typeof asset === 'function' ? asset.options && asset.options.name || asset.id : asset.name || asset.id;
+ if (!id) {
+ 'development' !== 'production' && warn('Array-syntax assets must provide a "name" or "id" field.');
+ } else {
+ res[id] = asset;
+ }
+ }
+ return res;
+ }
+ return assets;
+ }
+
+ /**
+ * Merge two option objects into a new one.
+ * Core utility used in both instantiation and inheritance.
+ *
+ * @param {Object} parent
+ * @param {Object} child
+ * @param {Vue} [vm] - if vm is present, indicates this is
+ * an instantiation merge.
+ */
+
+ function mergeOptions(parent, child, vm) {
+ guardComponents(child);
+ guardProps(child);
+ if ('development' !== 'production') {
+ if (child.propsData && !vm) {
+ warn('propsData can only be used as an instantiation option.');
+ }
+ }
+ var options = {};
+ var key;
+ if (child['extends']) {
+ parent = typeof child['extends'] === 'function' ? mergeOptions(parent, child['extends'].options, vm) : mergeOptions(parent, child['extends'], vm);
+ }
+ if (child.mixins) {
+ for (var i = 0, l = child.mixins.length; i < l; i++) {
+ var mixin = child.mixins[i];
+ var mixinOptions = mixin.prototype instanceof Vue ? mixin.options : mixin;
+ parent = mergeOptions(parent, mixinOptions, vm);
+ }
+ }
+ for (key in parent) {
+ mergeField(key);
+ }
+ for (key in child) {
+ if (!hasOwn(parent, key)) {
+ mergeField(key);
+ }
+ }
+ function mergeField(key) {
+ var strat = strats[key] || defaultStrat;
+ options[key] = strat(parent[key], child[key], vm, key);
+ }
+ return options;
+ }
+
+ /**
+ * Resolve an asset.
+ * This function is used because child instances need access
+ * to assets defined in its ancestor chain.
+ *
+ * @param {Object} options
+ * @param {String} type
+ * @param {String} id
+ * @param {Boolean} warnMissing
+ * @return {Object|Function}
+ */
+
+ function resolveAsset(options, type, id, warnMissing) {
+ /* istanbul ignore if */
+ if (typeof id !== 'string') {
+ return;
+ }
+ var assets = options[type];
+ var camelizedId;
+ var res = assets[id] ||
+ // camelCase ID
+ assets[camelizedId = camelize(id)] ||
+ // Pascal Case ID
+ assets[camelizedId.charAt(0).toUpperCase() + camelizedId.slice(1)];
+ if ('development' !== 'production' && warnMissing && !res) {
+ warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id, options);
+ }
+ return res;
+ }
+
+ var uid$1 = 0;
+
+ /**
+ * A dep is an observable that can have multiple
+ * directives subscribing to it.
+ *
+ * @constructor
+ */
+ function Dep() {
+ this.id = uid$1++;
+ this.subs = [];
+ }
+
+ // the current target watcher being evaluated.
+ // this is globally unique because there could be only one
+ // watcher being evaluated at any time.
+ Dep.target = null;
+
+ /**
+ * Add a directive subscriber.
+ *
+ * @param {Directive} sub
+ */
+
+ Dep.prototype.addSub = function (sub) {
+ this.subs.push(sub);
+ };
+
+ /**
+ * Remove a directive subscriber.
+ *
+ * @param {Directive} sub
+ */
+
+ Dep.prototype.removeSub = function (sub) {
+ this.subs.$remove(sub);
+ };
+
+ /**
+ * Add self as a dependency to the target watcher.
+ */
+
+ Dep.prototype.depend = function () {
+ Dep.target.addDep(this);
+ };
+
+ /**
+ * Notify all subscribers of a new value.
+ */
+
+ Dep.prototype.notify = function () {
+ // stablize the subscriber list first
+ var subs = toArray(this.subs);
+ for (var i = 0, l = subs.length; i < l; i++) {
+ subs[i].update();
+ }
+ };
+
+ var arrayProto = Array.prototype;
+ var arrayMethods = Object.create(arrayProto)
+
+ /**
+ * Intercept mutating methods and emit events
+ */
+
+ ;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
+ // cache original method
+ var original = arrayProto[method];
+ def(arrayMethods, method, function mutator() {
+ // avoid leaking arguments:
+ // http://jsperf.com/closure-with-arguments
+ var i = arguments.length;
+ var args = new Array(i);
+ while (i--) {
+ args[i] = arguments[i];
+ }
+ var result = original.apply(this, args);
+ var ob = this.__ob__;
+ var inserted;
+ switch (method) {
+ case 'push':
+ inserted = args;
+ break;
+ case 'unshift':
+ inserted = args;
+ break;
+ case 'splice':
+ inserted = args.slice(2);
+ break;
+ }
+ if (inserted) ob.observeArray(inserted);
+ // notify change
+ ob.dep.notify();
+ return result;
+ });
+ });
+
+ /**
+ * Swap the element at the given index with a new value
+ * and emits corresponding event.
+ *
+ * @param {Number} index
+ * @param {*} val
+ * @return {*} - replaced element
+ */
+
+ def(arrayProto, '$set', function $set(index, val) {
+ if (index >= this.length) {
+ this.length = Number(index) + 1;
+ }
+ return this.splice(index, 1, val)[0];
+ });
+
+ /**
+ * Convenience method to remove the element at given index or target element reference.
+ *
+ * @param {*} item
+ */
+
+ def(arrayProto, '$remove', function $remove(item) {
+ /* istanbul ignore if */
+ if (!this.length) return;
+ var index = indexOf(this, item);
+ if (index > -1) {
+ return this.splice(index, 1);
+ }
+ });
+
+ var arrayKeys = Object.getOwnPropertyNames(arrayMethods);
+
+ /**
+ * By default, when a reactive property is set, the new value is
+ * also converted to become reactive. However in certain cases, e.g.
+ * v-for scope alias and props, we don't want to force conversion
+ * because the value may be a nested value under a frozen data structure.
+ *
+ * So whenever we want to set a reactive property without forcing
+ * conversion on the new value, we wrap that call inside this function.
+ */
+
+ var shouldConvert = true;
+
+ function withoutConversion(fn) {
+ shouldConvert = false;
+ fn();
+ shouldConvert = true;
+ }
+
+ /**
+ * Observer class that are attached to each observed
+ * object. Once attached, the observer converts target
+ * object's property keys into getter/setters that
+ * collect dependencies and dispatches updates.
+ *
+ * @param {Array|Object} value
+ * @constructor
+ */
+
+ function Observer(value) {
+ this.value = value;
+ this.dep = new Dep();
+ def(value, '__ob__', this);
+ if (isArray(value)) {
+ var augment = hasProto ? protoAugment : copyAugment;
+ augment(value, arrayMethods, arrayKeys);
+ this.observeArray(value);
+ } else {
+ this.walk(value);
+ }
+ }
+
+ // Instance methods
+
+ /**
+ * Walk through each property and convert them into
+ * getter/setters. This method should only be called when
+ * value type is Object.
+ *
+ * @param {Object} obj
+ */
+
+ Observer.prototype.walk = function (obj) {
+ var keys = Object.keys(obj);
+ for (var i = 0, l = keys.length; i < l; i++) {
+ this.convert(keys[i], obj[keys[i]]);
+ }
+ };
+
+ /**
+ * Observe a list of Array items.
+ *
+ * @param {Array} items
+ */
+
+ Observer.prototype.observeArray = function (items) {
+ for (var i = 0, l = items.length; i < l; i++) {
+ observe(items[i]);
+ }
+ };
+
+ /**
+ * Convert a property into getter/setter so we can emit
+ * the events when the property is accessed/changed.
+ *
+ * @param {String} key
+ * @param {*} val
+ */
+
+ Observer.prototype.convert = function (key, val) {
+ defineReactive(this.value, key, val);
+ };
+
+ /**
+ * Add an owner vm, so that when $set/$delete mutations
+ * happen we can notify owner vms to proxy the keys and
+ * digest the watchers. This is only called when the object
+ * is observed as an instance's root $data.
+ *
+ * @param {Vue} vm
+ */
+
+ Observer.prototype.addVm = function (vm) {
+ (this.vms || (this.vms = [])).push(vm);
+ };
+
+ /**
+ * Remove an owner vm. This is called when the object is
+ * swapped out as an instance's $data object.
+ *
+ * @param {Vue} vm
+ */
+
+ Observer.prototype.removeVm = function (vm) {
+ this.vms.$remove(vm);
+ };
+
+ // helpers
+
+ /**
+ * Augment an target Object or Array by intercepting
+ * the prototype chain using __proto__
+ *
+ * @param {Object|Array} target
+ * @param {Object} src
+ */
+
+ function protoAugment(target, src) {
+ /* eslint-disable no-proto */
+ target.__proto__ = src;
+ /* eslint-enable no-proto */
+ }
+
+ /**
+ * Augment an target Object or Array by defining
+ * hidden properties.
+ *
+ * @param {Object|Array} target
+ * @param {Object} proto
+ */
+
+ function copyAugment(target, src, keys) {
+ for (var i = 0, l = keys.length; i < l; i++) {
+ var key = keys[i];
+ def(target, key, src[key]);
+ }
+ }
+
+ /**
+ * Attempt to create an observer instance for a value,
+ * returns the new observer if successfully observed,
+ * or the existing observer if the value already has one.
+ *
+ * @param {*} value
+ * @param {Vue} [vm]
+ * @return {Observer|undefined}
+ * @static
+ */
+
+ function observe(value, vm) {
+ if (!value || typeof value !== 'object') {
+ return;
+ }
+ var ob;
+ if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
+ ob = value.__ob__;
+ } else if (shouldConvert && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue) {
+ ob = new Observer(value);
+ }
+ if (ob && vm) {
+ ob.addVm(vm);
+ }
+ return ob;
+ }
+
+ /**
+ * Define a reactive property on an Object.
+ *
+ * @param {Object} obj
+ * @param {String} key
+ * @param {*} val
+ */
+
+ function defineReactive(obj, key, val) {
+ var dep = new Dep();
+
+ var property = Object.getOwnPropertyDescriptor(obj, key);
+ if (property && property.configurable === false) {
+ return;
+ }
+
+ // cater for pre-defined getter/setters
+ var getter = property && property.get;
+ var setter = property && property.set;
+
+ var childOb = observe(val);
+ Object.defineProperty(obj, key, {
+ enumerable: true,
+ configurable: true,
+ get: function reactiveGetter() {
+ var value = getter ? getter.call(obj) : val;
+ if (Dep.target) {
+ dep.depend();
+ if (childOb) {
+ childOb.dep.depend();
+ }
+ if (isArray(value)) {
+ for (var e, i = 0, l = value.length; i < l; i++) {
+ e = value[i];
+ e && e.__ob__ && e.__ob__.dep.depend();
+ }
+ }
+ }
+ return value;
+ },
+ set: function reactiveSetter(newVal) {
+ var value = getter ? getter.call(obj) : val;
+ if (newVal === value) {
+ return;
+ }
+ if (setter) {
+ setter.call(obj, newVal);
+ } else {
+ val = newVal;
+ }
+ childOb = observe(newVal);
+ dep.notify();
+ }
+ });
+ }
+
+
+
+ var util = Object.freeze({
+ defineReactive: defineReactive,
+ set: set,
+ del: del,
+ hasOwn: hasOwn,
+ isLiteral: isLiteral,
+ isReserved: isReserved,
+ _toString: _toString,
+ toNumber: toNumber,
+ toBoolean: toBoolean,
+ stripQuotes: stripQuotes,
+ camelize: camelize,
+ hyphenate: hyphenate,
+ classify: classify,
+ bind: bind,
+ toArray: toArray,
+ extend: extend,
+ isObject: isObject,
+ isPlainObject: isPlainObject,
+ def: def,
+ debounce: _debounce,
+ indexOf: indexOf,
+ cancellable: cancellable,
+ looseEqual: looseEqual,
+ isArray: isArray,
+ hasProto: hasProto,
+ inBrowser: inBrowser,
+ devtools: devtools,
+ isIE: isIE,
+ isIE9: isIE9,
+ isAndroid: isAndroid,
+ isIos: isIos,
+ iosVersionMatch: iosVersionMatch,
+ iosVersion: iosVersion,
+ hasMutationObserverBug: hasMutationObserverBug,
+ get transitionProp () { return transitionProp; },
+ get transitionEndEvent () { return transitionEndEvent; },
+ get animationProp () { return animationProp; },
+ get animationEndEvent () { return animationEndEvent; },
+ nextTick: nextTick,
+ get _Set () { return _Set; },
+ query: query,
+ inDoc: inDoc,
+ getAttr: getAttr,
+ getBindAttr: getBindAttr,
+ hasBindAttr: hasBindAttr,
+ before: before,
+ after: after,
+ remove: remove,
+ prepend: prepend,
+ replace: replace,
+ on: on,
+ off: off,
+ setClass: setClass,
+ addClass: addClass,
+ removeClass: removeClass,
+ extractContent: extractContent,
+ trimNode: trimNode,
+ isTemplate: isTemplate,
+ createAnchor: createAnchor,
+ findRef: findRef,
+ mapNodeRange: mapNodeRange,
+ removeNodeRange: removeNodeRange,
+ isFragment: isFragment,
+ getOuterHTML: getOuterHTML,
+ mergeOptions: mergeOptions,
+ resolveAsset: resolveAsset,
+ checkComponentAttr: checkComponentAttr,
+ commonTagRE: commonTagRE,
+ reservedTagRE: reservedTagRE,
+ get warn () { return warn; }
+ });
+
+ var uid = 0;
+
+ function initMixin (Vue) {
+ /**
+ * The main init sequence. This is called for every
+ * instance, including ones that are created from extended
+ * constructors.
+ *
+ * @param {Object} options - this options object should be
+ * the result of merging class
+ * options and the options passed
+ * in to the constructor.
+ */
+
+ Vue.prototype._init = function (options) {
+ options = options || {};
+
+ this.$el = null;
+ this.$parent = options.parent;
+ this.$root = this.$parent ? this.$parent.$root : this;
+ this.$children = [];
+ this.$refs = {}; // child vm references
+ this.$els = {}; // element references
+ this._watchers = []; // all watchers as an array
+ this._directives = []; // all directives
+
+ // a uid
+ this._uid = uid++;
+
+ // a flag to avoid this being observed
+ this._isVue = true;
+
+ // events bookkeeping
+ this._events = {}; // registered callbacks
+ this._eventsCount = {}; // for $broadcast optimization
+
+ // fragment instance properties
+ this._isFragment = false;
+ this._fragment = // @type {DocumentFragment}
+ this._fragmentStart = // @type {Text|Comment}
+ this._fragmentEnd = null; // @type {Text|Comment}
+
+ // lifecycle state
+ this._isCompiled = this._isDestroyed = this._isReady = this._isAttached = this._isBeingDestroyed = this._vForRemoving = false;
+ this._unlinkFn = null;
+
+ // context:
+ // if this is a transcluded component, context
+ // will be the common parent vm of this instance
+ // and its host.
+ this._context = options._context || this.$parent;
+
+ // scope:
+ // if this is inside an inline v-for, the scope
+ // will be the intermediate scope created for this
+ // repeat fragment. this is used for linking props
+ // and container directives.
+ this._scope = options._scope;
+
+ // fragment:
+ // if this instance is compiled inside a Fragment, it
+ // needs to reigster itself as a child of that fragment
+ // for attach/detach to work properly.
+ this._frag = options._frag;
+ if (this._frag) {
+ this._frag.children.push(this);
+ }
+
+ // push self into parent / transclusion host
+ if (this.$parent) {
+ this.$parent.$children.push(this);
+ }
+
+ // merge options.
+ options = this.$options = mergeOptions(this.constructor.options, options, this);
+
+ // set ref
+ this._updateRef();
+
+ // initialize data as empty object.
+ // it will be filled up in _initData().
+ this._data = {};
+
+ // call init hook
+ this._callHook('init');
+
+ // initialize data observation and scope inheritance.
+ this._initState();
+
+ // setup event system and option events.
+ this._initEvents();
+
+ // call created hook
+ this._callHook('created');
+
+ // if `el` option is passed, start compilation.
+ if (options.el) {
+ this.$mount(options.el);
+ }
+ };
+ }
+
+ var pathCache = new Cache(1000);
+
+ // actions
+ var APPEND = 0;
+ var PUSH = 1;
+ var INC_SUB_PATH_DEPTH = 2;
+ var PUSH_SUB_PATH = 3;
+
+ // states
+ var BEFORE_PATH = 0;
+ var IN_PATH = 1;
+ var BEFORE_IDENT = 2;
+ var IN_IDENT = 3;
+ var IN_SUB_PATH = 4;
+ var IN_SINGLE_QUOTE = 5;
+ var IN_DOUBLE_QUOTE = 6;
+ var AFTER_PATH = 7;
+ var ERROR = 8;
+
+ var pathStateMachine = [];
+
+ pathStateMachine[BEFORE_PATH] = {
+ 'ws': [BEFORE_PATH],
+ 'ident': [IN_IDENT, APPEND],
+ '[': [IN_SUB_PATH],
+ 'eof': [AFTER_PATH]
+ };
+
+ pathStateMachine[IN_PATH] = {
+ 'ws': [IN_PATH],
+ '.': [BEFORE_IDENT],
+ '[': [IN_SUB_PATH],
+ 'eof': [AFTER_PATH]
+ };
+
+ pathStateMachine[BEFORE_IDENT] = {
+ 'ws': [BEFORE_IDENT],
+ 'ident': [IN_IDENT, APPEND]
+ };
+
+ pathStateMachine[IN_IDENT] = {
+ 'ident': [IN_IDENT, APPEND],
+ '0': [IN_IDENT, APPEND],
+ 'number': [IN_IDENT, APPEND],
+ 'ws': [IN_PATH, PUSH],
+ '.': [BEFORE_IDENT, PUSH],
+ '[': [IN_SUB_PATH, PUSH],
+ 'eof': [AFTER_PATH, PUSH]
+ };
+
+ pathStateMachine[IN_SUB_PATH] = {
+ "'": [IN_SINGLE_QUOTE, APPEND],
+ '"': [IN_DOUBLE_QUOTE, APPEND],
+ '[': [IN_SUB_PATH, INC_SUB_PATH_DEPTH],
+ ']': [IN_PATH, PUSH_SUB_PATH],
+ 'eof': ERROR,
+ 'else': [IN_SUB_PATH, APPEND]
+ };
+
+ pathStateMachine[IN_SINGLE_QUOTE] = {
+ "'": [IN_SUB_PATH, APPEND],
+ 'eof': ERROR,
+ 'else': [IN_SINGLE_QUOTE, APPEND]
+ };
+
+ pathStateMachine[IN_DOUBLE_QUOTE] = {
+ '"': [IN_SUB_PATH, APPEND],
+ 'eof': ERROR,
+ 'else': [IN_DOUBLE_QUOTE, APPEND]
+ };
+
+ /**
+ * Determine the type of a character in a keypath.
+ *
+ * @param {Char} ch
+ * @return {String} type
+ */
+
+ function getPathCharType(ch) {
+ if (ch === undefined) {
+ return 'eof';
+ }
+
+ var code = ch.charCodeAt(0);
+
+ switch (code) {
+ case 0x5B: // [
+ case 0x5D: // ]
+ case 0x2E: // .
+ case 0x22: // "
+ case 0x27: // '
+ case 0x30:
+ // 0
+ return ch;
+
+ case 0x5F: // _
+ case 0x24:
+ // $
+ return 'ident';
+
+ case 0x20: // Space
+ case 0x09: // Tab
+ case 0x0A: // Newline
+ case 0x0D: // Return
+ case 0xA0: // No-break space
+ case 0xFEFF: // Byte Order Mark
+ case 0x2028: // Line Separator
+ case 0x2029:
+ // Paragraph Separator
+ return 'ws';
+ }
+
+ // a-z, A-Z
+ if (code >= 0x61 && code <= 0x7A || code >= 0x41 && code <= 0x5A) {
+ return 'ident';
+ }
+
+ // 1-9
+ if (code >= 0x31 && code <= 0x39) {
+ return 'number';
+ }
+
+ return 'else';
+ }
+
+ /**
+ * Format a subPath, return its plain form if it is
+ * a literal string or number. Otherwise prepend the
+ * dynamic indicator (*).
+ *
+ * @param {String} path
+ * @return {String}
+ */
+
+ function formatSubPath(path) {
+ var trimmed = path.trim();
+ // invalid leading 0
+ if (path.charAt(0) === '0' && isNaN(path)) {
+ return false;
+ }
+ return isLiteral(trimmed) ? stripQuotes(trimmed) : '*' + trimmed;
+ }
+
+ /**
+ * Parse a string path into an array of segments
+ *
+ * @param {String} path
+ * @return {Array|undefined}
+ */
+
+ function parse(path) {
+ var keys = [];
+ var index = -1;
+ var mode = BEFORE_PATH;
+ var subPathDepth = 0;
+ var c, newChar, key, type, transition, action, typeMap;
+
+ var actions = [];
+
+ actions[PUSH] = function () {
+ if (key !== undefined) {
+ keys.push(key);
+ key = undefined;
+ }
+ };
+
+ actions[APPEND] = function () {
+ if (key === undefined) {
+ key = newChar;
+ } else {
+ key += newChar;
+ }
+ };
+
+ actions[INC_SUB_PATH_DEPTH] = function () {
+ actions[APPEND]();
+ subPathDepth++;
+ };
+
+ actions[PUSH_SUB_PATH] = function () {
+ if (subPathDepth > 0) {
+ subPathDepth--;
+ mode = IN_SUB_PATH;
+ actions[APPEND]();
+ } else {
+ subPathDepth = 0;
+ key = formatSubPath(key);
+ if (key === false) {
+ return false;
+ } else {
+ actions[PUSH]();
+ }
+ }
+ };
+
+ function maybeUnescapeQuote() {
+ var nextChar = path[index + 1];
+ if (mode === IN_SINGLE_QUOTE && nextChar === "'" || mode === IN_DOUBLE_QUOTE && nextChar === '"') {
+ index++;
+ newChar = '\\' + nextChar;
+ actions[APPEND]();
+ return true;
+ }
+ }
+
+ while (mode != null) {
+ index++;
+ c = path[index];
+
+ if (c === '\\' && maybeUnescapeQuote()) {
+ continue;
+ }
+
+ type = getPathCharType(c);
+ typeMap = pathStateMachine[mode];
+ transition = typeMap[type] || typeMap['else'] || ERROR;
+
+ if (transition === ERROR) {
+ return; // parse error
+ }
+
+ mode = transition[0];
+ action = actions[transition[1]];
+ if (action) {
+ newChar = transition[2];
+ newChar = newChar === undefined ? c : newChar;
+ if (action() === false) {
+ return;
+ }
+ }
+
+ if (mode === AFTER_PATH) {
+ keys.raw = path;
+ return keys;
+ }
+ }
+ }
+
+ /**
+ * External parse that check for a cache hit first
+ *
+ * @param {String} path
+ * @return {Array|undefined}
+ */
+
+ function parsePath(path) {
+ var hit = pathCache.get(path);
+ if (!hit) {
+ hit = parse(path);
+ if (hit) {
+ pathCache.put(path, hit);
+ }
+ }
+ return hit;
+ }
+
+ /**
+ * Get from an object from a path string
+ *
+ * @param {Object} obj
+ * @param {String} path
+ */
+
+ function getPath(obj, path) {
+ return parseExpression(path).get(obj);
+ }
+
+ /**
+ * Warn against setting non-existent root path on a vm.
+ */
+
+ var warnNonExistent;
+ if ('development' !== 'production') {
+ warnNonExistent = function (path, vm) {
+ warn('You are setting a non-existent path "' + path.raw + '" ' + 'on a vm instance. Consider pre-initializing the property ' + 'with the "data" option for more reliable reactivity ' + 'and better performance.', vm);
+ };
+ }
+
+ /**
+ * Set on an object from a path
+ *
+ * @param {Object} obj
+ * @param {String | Array} path
+ * @param {*} val
+ */
+
+ function setPath(obj, path, val) {
+ var original = obj;
+ if (typeof path === 'string') {
+ path = parse(path);
+ }
+ if (!path || !isObject(obj)) {
+ return false;
+ }
+ var last, key;
+ for (var i = 0, l = path.length; i < l; i++) {
+ last = obj;
+ key = path[i];
+ if (key.charAt(0) === '*') {
+ key = parseExpression(key.slice(1)).get.call(original, original);
+ }
+ if (i < l - 1) {
+ obj = obj[key];
+ if (!isObject(obj)) {
+ obj = {};
+ if ('development' !== 'production' && last._isVue) {
+ warnNonExistent(path, last);
+ }
+ set(last, key, obj);
+ }
+ } else {
+ if (isArray(obj)) {
+ obj.$set(key, val);
+ } else if (key in obj) {
+ obj[key] = val;
+ } else {
+ if ('development' !== 'production' && obj._isVue) {
+ warnNonExistent(path, obj);
+ }
+ set(obj, key, val);
+ }
+ }
+ }
+ return true;
+ }
+
+var path = Object.freeze({
+ parsePath: parsePath,
+ getPath: getPath,
+ setPath: setPath
+ });
+
+ var expressionCache = new Cache(1000);
+
+ var allowedKeywords = 'Math,Date,this,true,false,null,undefined,Infinity,NaN,' + 'isNaN,isFinite,decodeURI,decodeURIComponent,encodeURI,' + 'encodeURIComponent,parseInt,parseFloat';
+ var allowedKeywordsRE = new RegExp('^(' + allowedKeywords.replace(/,/g, '\\b|') + '\\b)');
+
+ // keywords that don't make sense inside expressions
+ var improperKeywords = 'break,case,class,catch,const,continue,debugger,default,' + 'delete,do,else,export,extends,finally,for,function,if,' + 'import,in,instanceof,let,return,super,switch,throw,try,' + 'var,while,with,yield,enum,await,implements,package,' + 'protected,static,interface,private,public';
+ var improperKeywordsRE = new RegExp('^(' + improperKeywords.replace(/,/g, '\\b|') + '\\b)');
+
+ var wsRE = /\s/g;
+ var newlineRE = /\n/g;
+ var saveRE = /[\{,]\s*[\w\$_]+\s*:|('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`)|new |typeof |void /g;
+ var restoreRE = /"(\d+)"/g;
+ var pathTestRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*$/;
+ var identRE = /[^\w$\.](?:[A-Za-z_$][\w$]*)/g;
+ var literalValueRE$1 = /^(?:true|false|null|undefined|Infinity|NaN)$/;
+
+ function noop() {}
+
+ /**
+ * Save / Rewrite / Restore
+ *
+ * When rewriting paths found in an expression, it is
+ * possible for the same letter sequences to be found in
+ * strings and Object literal property keys. Therefore we
+ * remove and store these parts in a temporary array, and
+ * restore them after the path rewrite.
+ */
+
+ var saved = [];
+
+ /**
+ * Save replacer
+ *
+ * The save regex can match two possible cases:
+ * 1. An opening object literal
+ * 2. A string
+ * If matched as a plain string, we need to escape its
+ * newlines, since the string needs to be preserved when
+ * generating the function body.
+ *
+ * @param {String} str
+ * @param {String} isString - str if matched as a string
+ * @return {String} - placeholder with index
+ */
+
+ function save(str, isString) {
+ var i = saved.length;
+ saved[i] = isString ? str.replace(newlineRE, '\\n') : str;
+ return '"' + i + '"';
+ }
+
+ /**
+ * Path rewrite replacer
+ *
+ * @param {String} raw
+ * @return {String}
+ */
+
+ function rewrite(raw) {
+ var c = raw.charAt(0);
+ var path = raw.slice(1);
+ if (allowedKeywordsRE.test(path)) {
+ return raw;
+ } else {
+ path = path.indexOf('"') > -1 ? path.replace(restoreRE, restore) : path;
+ return c + 'scope.' + path;
+ }
+ }
+
+ /**
+ * Restore replacer
+ *
+ * @param {String} str
+ * @param {String} i - matched save index
+ * @return {String}
+ */
+
+ function restore(str, i) {
+ return saved[i];
+ }
+
+ /**
+ * Rewrite an expression, prefixing all path accessors with
+ * `scope.` and generate getter/setter functions.
+ *
+ * @param {String} exp
+ * @return {Function}
+ */
+
+ function compileGetter(exp) {
+ if (improperKeywordsRE.test(exp)) {
+ 'development' !== 'production' && warn('Avoid using reserved keywords in expression: ' + exp);
+ }
+ // reset state
+ saved.length = 0;
+ // save strings and object literal keys
+ var body = exp.replace(saveRE, save).replace(wsRE, '');
+ // rewrite all paths
+ // pad 1 space here because the regex matches 1 extra char
+ body = (' ' + body).replace(identRE, rewrite).replace(restoreRE, restore);
+ return makeGetterFn(body);
+ }
+
+ /**
+ * Build a getter function. Requires eval.
+ *
+ * We isolate the try/catch so it doesn't affect the
+ * optimization of the parse function when it is not called.
+ *
+ * @param {String} body
+ * @return {Function|undefined}
+ */
+
+ function makeGetterFn(body) {
+ try {
+ /* eslint-disable no-new-func */
+ return new Function('scope', 'return ' + body + ';');
+ /* eslint-enable no-new-func */
+ } catch (e) {
+ if ('development' !== 'production') {
+ /* istanbul ignore if */
+ if (e.toString().match(/unsafe-eval|CSP/)) {
+ warn('It seems you are using the default build of Vue.js in an environment ' + 'with Content Security Policy that prohibits unsafe-eval. ' + 'Use the CSP-compliant build instead: ' + 'http://vuejs.org/guide/installation.html#CSP-compliant-build');
+ } else {
+ warn('Invalid expression. ' + 'Generated function body: ' + body);
+ }
+ }
+ return noop;
+ }
+ }
+
+ /**
+ * Compile a setter function for the expression.
+ *
+ * @param {String} exp
+ * @return {Function|undefined}
+ */
+
+ function compileSetter(exp) {
+ var path = parsePath(exp);
+ if (path) {
+ return function (scope, val) {
+ setPath(scope, path, val);
+ };
+ } else {
+ 'development' !== 'production' && warn('Invalid setter expression: ' + exp);
+ }
+ }
+
+ /**
+ * Parse an expression into re-written getter/setters.
+ *
+ * @param {String} exp
+ * @param {Boolean} needSet
+ * @return {Function}
+ */
+
+ function parseExpression(exp, needSet) {
+ exp = exp.trim();
+ // try cache
+ var hit = expressionCache.get(exp);
+ if (hit) {
+ if (needSet && !hit.set) {
+ hit.set = compileSetter(hit.exp);
+ }
+ return hit;
+ }
+ var res = { exp: exp };
+ res.get = isSimplePath(exp) && exp.indexOf('[') < 0
+ // optimized super simple getter
+ ? makeGetterFn('scope.' + exp)
+ // dynamic getter
+ : compileGetter(exp);
+ if (needSet) {
+ res.set = compileSetter(exp);
+ }
+ expressionCache.put(exp, res);
+ return res;
+ }
+
+ /**
+ * Check if an expression is a simple path.
+ *
+ * @param {String} exp
+ * @return {Boolean}
+ */
+
+ function isSimplePath(exp) {
+ return pathTestRE.test(exp) &&
+ // don't treat literal values as paths
+ !literalValueRE$1.test(exp) &&
+ // Math constants e.g. Math.PI, Math.E etc.
+ exp.slice(0, 5) !== 'Math.';
+ }
+
+var expression = Object.freeze({
+ parseExpression: parseExpression,
+ isSimplePath: isSimplePath
+ });
+
+ // we have two separate queues: one for directive updates
+ // and one for user watcher registered via $watch().
+ // we want to guarantee directive updates to be called
+ // before user watchers so that when user watchers are
+ // triggered, the DOM would have already been in updated
+ // state.
+
+ var queue = [];
+ var userQueue = [];
+ var has = {};
+ var circular = {};
+ var waiting = false;
+
+ /**
+ * Reset the batcher's state.
+ */
+
+ function resetBatcherState() {
+ queue.length = 0;
+ userQueue.length = 0;
+ has = {};
+ circular = {};
+ waiting = false;
+ }
+
+ /**
+ * Flush both queues and run the watchers.
+ */
+
+ function flushBatcherQueue() {
+ var _again = true;
+
+ _function: while (_again) {
+ _again = false;
+
+ runBatcherQueue(queue);
+ runBatcherQueue(userQueue);
+ // user watchers triggered more watchers,
+ // keep flushing until it depletes
+ if (queue.length) {
+ _again = true;
+ continue _function;
+ }
+ // dev tool hook
+ /* istanbul ignore if */
+ if (devtools && config.devtools) {
+ devtools.emit('flush');
+ }
+ resetBatcherState();
+ }
+ }
+
+ /**
+ * Run the watchers in a single queue.
+ *
+ * @param {Array} queue
+ */
+
+ function runBatcherQueue(queue) {
+ // do not cache length because more watchers might be pushed
+ // as we run existing watchers
+ for (var i = 0; i < queue.length; i++) {
+ var watcher = queue[i];
+ var id = watcher.id;
+ has[id] = null;
+ watcher.run();
+ // in dev build, check and stop circular updates.
+ if ('development' !== 'production' && has[id] != null) {
+ circular[id] = (circular[id] || 0) + 1;
+ if (circular[id] > config._maxUpdateCount) {
+ warn('You may have an infinite update loop for watcher ' + 'with expression "' + watcher.expression + '"', watcher.vm);
+ break;
+ }
+ }
+ }
+ queue.length = 0;
+ }
+
+ /**
+ * Push a watcher into the watcher queue.
+ * Jobs with duplicate IDs will be skipped unless it's
+ * pushed when the queue is being flushed.
+ *
+ * @param {Watcher} watcher
+ * properties:
+ * - {Number} id
+ * - {Function} run
+ */
+
+ function pushWatcher(watcher) {
+ var id = watcher.id;
+ if (has[id] == null) {
+ // push watcher into appropriate queue
+ var q = watcher.user ? userQueue : queue;
+ has[id] = q.length;
+ q.push(watcher);
+ // queue the flush
+ if (!waiting) {
+ waiting = true;
+ nextTick(flushBatcherQueue);
+ }
+ }
+ }
+
+ var uid$2 = 0;
+
+ /**
+ * A watcher parses an expression, collects dependencies,
+ * and fires callback when the expression value changes.
+ * This is used for both the $watch() api and directives.
+ *
+ * @param {Vue} vm
+ * @param {String|Function} expOrFn
+ * @param {Function} cb
+ * @param {Object} options
+ * - {Array} filters
+ * - {Boolean} twoWay
+ * - {Boolean} deep
+ * - {Boolean} user
+ * - {Boolean} sync
+ * - {Boolean} lazy
+ * - {Function} [preProcess]
+ * - {Function} [postProcess]
+ * @constructor
+ */
+ function Watcher(vm, expOrFn, cb, options) {
+ // mix in options
+ if (options) {
+ extend(this, options);
+ }
+ var isFn = typeof expOrFn === 'function';
+ this.vm = vm;
+ vm._watchers.push(this);
+ this.expression = expOrFn;
+ this.cb = cb;
+ this.id = ++uid$2; // uid for batching
+ this.active = true;
+ this.dirty = this.lazy; // for lazy watchers
+ this.deps = [];
+ this.newDeps = [];
+ this.depIds = new _Set();
+ this.newDepIds = new _Set();
+ this.prevError = null; // for async error stacks
+ // parse expression for getter/setter
+ if (isFn) {
+ this.getter = expOrFn;
+ this.setter = undefined;
+ } else {
+ var res = parseExpression(expOrFn, this.twoWay);
+ this.getter = res.get;
+ this.setter = res.set;
+ }
+ this.value = this.lazy ? undefined : this.get();
+ // state for avoiding false triggers for deep and Array
+ // watchers during vm._digest()
+ this.queued = this.shallow = false;
+ }
+
+ /**
+ * Evaluate the getter, and re-collect dependencies.
+ */
+
+ Watcher.prototype.get = function () {
+ this.beforeGet();
+ var scope = this.scope || this.vm;
+ var value;
+ try {
+ value = this.getter.call(scope, scope);
+ } catch (e) {
+ if ('development' !== 'production' && config.warnExpressionErrors) {
+ warn('Error when evaluating expression ' + '"' + this.expression + '": ' + e.toString(), this.vm);
+ }
+ }
+ // "touch" every property so they are all tracked as
+ // dependencies for deep watching
+ if (this.deep) {
+ traverse(value);
+ }
+ if (this.preProcess) {
+ value = this.preProcess(value);
+ }
+ if (this.filters) {
+ value = scope._applyFilters(value, null, this.filters, false);
+ }
+ if (this.postProcess) {
+ value = this.postProcess(value);
+ }
+ this.afterGet();
+ return value;
+ };
+
+ /**
+ * Set the corresponding value with the setter.
+ *
+ * @param {*} value
+ */
+
+ Watcher.prototype.set = function (value) {
+ var scope = this.scope || this.vm;
+ if (this.filters) {
+ value = scope._applyFilters(value, this.value, this.filters, true);
+ }
+ try {
+ this.setter.call(scope, scope, value);
+ } catch (e) {
+ if ('development' !== 'production' && config.warnExpressionErrors) {
+ warn('Error when evaluating setter ' + '"' + this.expression + '": ' + e.toString(), this.vm);
+ }
+ }
+ // two-way sync for v-for alias
+ var forContext = scope.$forContext;
+ if (forContext && forContext.alias === this.expression) {
+ if (forContext.filters) {
+ 'development' !== 'production' && warn('It seems you are using two-way binding on ' + 'a v-for alias (' + this.expression + '), and the ' + 'v-for has filters. This will not work properly. ' + 'Either remove the filters or use an array of ' + 'objects and bind to object properties instead.', this.vm);
+ return;
+ }
+ forContext._withLock(function () {
+ if (scope.$key) {
+ // original is an object
+ forContext.rawValue[scope.$key] = value;
+ } else {
+ forContext.rawValue.$set(scope.$index, value);
+ }
+ });
+ }
+ };
+
+ /**
+ * Prepare for dependency collection.
+ */
+
+ Watcher.prototype.beforeGet = function () {
+ Dep.target = this;
+ };
+
+ /**
+ * Add a dependency to this directive.
+ *
+ * @param {Dep} dep
+ */
+
+ Watcher.prototype.addDep = function (dep) {
+ var id = dep.id;
+ if (!this.newDepIds.has(id)) {
+ this.newDepIds.add(id);
+ this.newDeps.push(dep);
+ if (!this.depIds.has(id)) {
+ dep.addSub(this);
+ }
+ }
+ };
+
+ /**
+ * Clean up for dependency collection.
+ */
+
+ Watcher.prototype.afterGet = function () {
+ Dep.target = null;
+ var i = this.deps.length;
+ while (i--) {
+ var dep = this.deps[i];
+ if (!this.newDepIds.has(dep.id)) {
+ dep.removeSub(this);
+ }
+ }
+ var tmp = this.depIds;
+ this.depIds = this.newDepIds;
+ this.newDepIds = tmp;
+ this.newDepIds.clear();
+ tmp = this.deps;
+ this.deps = this.newDeps;
+ this.newDeps = tmp;
+ this.newDeps.length = 0;
+ };
+
+ /**
+ * Subscriber interface.
+ * Will be called when a dependency changes.
+ *
+ * @param {Boolean} shallow
+ */
+
+ Watcher.prototype.update = function (shallow) {
+ if (this.lazy) {
+ this.dirty = true;
+ } else if (this.sync || !config.async) {
+ this.run();
+ } else {
+ // if queued, only overwrite shallow with non-shallow,
+ // but not the other way around.
+ this.shallow = this.queued ? shallow ? this.shallow : false : !!shallow;
+ this.queued = true;
+ // record before-push error stack in debug mode
+ /* istanbul ignore if */
+ if ('development' !== 'production' && config.debug) {
+ this.prevError = new Error('[vue] async stack trace');
+ }
+ pushWatcher(this);
+ }
+ };
+
+ /**
+ * Batcher job interface.
+ * Will be called by the batcher.
+ */
+
+ Watcher.prototype.run = function () {
+ if (this.active) {
+ var value = this.get();
+ if (value !== this.value ||
+ // Deep watchers and watchers on Object/Arrays should fire even
+ // when the value is the same, because the value may
+ // have mutated; but only do so if this is a
+ // non-shallow update (caused by a vm digest).
+ (isObject(value) || this.deep) && !this.shallow) {
+ // set new value
+ var oldValue = this.value;
+ this.value = value;
+ // in debug + async mode, when a watcher callbacks
+ // throws, we also throw the saved before-push error
+ // so the full cross-tick stack trace is available.
+ var prevError = this.prevError;
+ /* istanbul ignore if */
+ if ('development' !== 'production' && config.debug && prevError) {
+ this.prevError = null;
+ try {
+ this.cb.call(this.vm, value, oldValue);
+ } catch (e) {
+ nextTick(function () {
+ throw prevError;
+ }, 0);
+ throw e;
+ }
+ } else {
+ this.cb.call(this.vm, value, oldValue);
+ }
+ }
+ this.queued = this.shallow = false;
+ }
+ };
+
+ /**
+ * Evaluate the value of the watcher.
+ * This only gets called for lazy watchers.
+ */
+
+ Watcher.prototype.evaluate = function () {
+ // avoid overwriting another watcher that is being
+ // collected.
+ var current = Dep.target;
+ this.value = this.get();
+ this.dirty = false;
+ Dep.target = current;
+ };
+
+ /**
+ * Depend on all deps collected by this watcher.
+ */
+
+ Watcher.prototype.depend = function () {
+ var i = this.deps.length;
+ while (i--) {
+ this.deps[i].depend();
+ }
+ };
+
+ /**
+ * Remove self from all dependencies' subcriber list.
+ */
+
+ Watcher.prototype.teardown = function () {
+ if (this.active) {
+ // remove self from vm's watcher list
+ // this is a somewhat expensive operation so we skip it
+ // if the vm is being destroyed or is performing a v-for
+ // re-render (the watcher list is then filtered by v-for).
+ if (!this.vm._isBeingDestroyed && !this.vm._vForRemoving) {
+ this.vm._watchers.$remove(this);
+ }
+ var i = this.deps.length;
+ while (i--) {
+ this.deps[i].removeSub(this);
+ }
+ this.active = false;
+ this.vm = this.cb = this.value = null;
+ }
+ };
+
+ /**
+ * Recrusively traverse an object to evoke all converted
+ * getters, so that every nested property inside the object
+ * is collected as a "deep" dependency.
+ *
+ * @param {*} val
+ */
+
+ var seenObjects = new _Set();
+ function traverse(val, seen) {
+ var i = undefined,
+ keys = undefined;
+ if (!seen) {
+ seen = seenObjects;
+ seen.clear();
+ }
+ var isA = isArray(val);
+ var isO = isObject(val);
+ if ((isA || isO) && Object.isExtensible(val)) {
+ if (val.__ob__) {
+ var depId = val.__ob__.dep.id;
+ if (seen.has(depId)) {
+ return;
+ } else {
+ seen.add(depId);
+ }
+ }
+ if (isA) {
+ i = val.length;
+ while (i--) traverse(val[i], seen);
+ } else if (isO) {
+ keys = Object.keys(val);
+ i = keys.length;
+ while (i--) traverse(val[keys[i]], seen);
+ }
+ }
+ }
+
+ var text$1 = {
+
+ bind: function bind() {
+ this.attr = this.el.nodeType === 3 ? 'data' : 'textContent';
+ },
+
+ update: function update(value) {
+ this.el[this.attr] = _toString(value);
+ }
+ };
+
+ var templateCache = new Cache(1000);
+ var idSelectorCache = new Cache(1000);
+
+ var map = {
+ efault: [0, '', ''],
+ legend: [1, '<fieldset>', '</fieldset>'],
+ tr: [2, '<table><tbody>', '</tbody></table>'],
+ col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>']
+ };
+
+ map.td = map.th = [3, '<table><tbody><tr>', '</tr></tbody></table>'];
+
+ map.option = map.optgroup = [1, '<select multiple="multiple">', '</select>'];
+
+ map.thead = map.tbody = map.colgroup = map.caption = map.tfoot = [1, '<table>', '</table>'];
+
+ map.g = map.defs = map.symbol = map.use = map.image = map.text = map.circle = map.ellipse = map.line = map.path = map.polygon = map.polyline = map.rect = [1, '<svg ' + 'xmlns="http://www.w3.org/2000/svg" ' + 'xmlns:xlink="http://www.w3.org/1999/xlink" ' + 'xmlns:ev="http://www.w3.org/2001/xml-events"' + 'version="1.1">', '</svg>'];
+
+ /**
+ * Check if a node is a supported template node with a
+ * DocumentFragment content.
+ *
+ * @param {Node} node
+ * @return {Boolean}
+ */
+
+ function isRealTemplate(node) {
+ return isTemplate(node) && isFragment(node.content);
+ }
+
+ var tagRE$1 = /<([\w:-]+)/;
+ var entityRE = /&#?\w+?;/;
+ var commentRE = /<!--/;
+
+ /**
+ * Convert a string template to a DocumentFragment.
+ * Determines correct wrapping by tag types. Wrapping
+ * strategy found in jQuery & component/domify.
+ *
+ * @param {String} templateString
+ * @param {Boolean} raw
+ * @return {DocumentFragment}
+ */
+
+ function stringToFragment(templateString, raw) {
+ // try a cache hit first
+ var cacheKey = raw ? templateString : templateString.trim();
+ var hit = templateCache.get(cacheKey);
+ if (hit) {
+ return hit;
+ }
+
+ var frag = document.createDocumentFragment();
+ var tagMatch = templateString.match(tagRE$1);
+ var entityMatch = entityRE.test(templateString);
+ var commentMatch = commentRE.test(templateString);
+
+ if (!tagMatch && !entityMatch && !commentMatch) {
+ // text only, return a single text node.
+ frag.appendChild(document.createTextNode(templateString));
+ } else {
+ var tag = tagMatch && tagMatch[1];
+ var wrap = map[tag] || map.efault;
+ var depth = wrap[0];
+ var prefix = wrap[1];
+ var suffix = wrap[2];
+ var node = document.createElement('div');
+
+ node.innerHTML = prefix + templateString + suffix;
+ while (depth--) {
+ node = node.lastChild;
+ }
+
+ var child;
+ /* eslint-disable no-cond-assign */
+ while (child = node.firstChild) {
+ /* eslint-enable no-cond-assign */
+ frag.appendChild(child);
+ }
+ }
+ if (!raw) {
+ trimNode(frag);
+ }
+ templateCache.put(cacheKey, frag);
+ return frag;
+ }
+
+ /**
+ * Convert a template node to a DocumentFragment.
+ *
+ * @param {Node} node
+ * @return {DocumentFragment}
+ */
+
+ function nodeToFragment(node) {
+ // if its a template tag and the browser supports it,
+ // its content is already a document fragment. However, iOS Safari has
+ // bug when using directly cloned template content with touch
+ // events and can cause crashes when the nodes are removed from DOM, so we
+ // have to treat template elements as string templates. (#2805)
+ /* istanbul ignore if */
+ if (isRealTemplate(node)) {
+ return stringToFragment(node.innerHTML);
+ }
+ // script template
+ if (node.tagName === 'SCRIPT') {
+ return stringToFragment(node.textContent);
+ }
+ // normal node, clone it to avoid mutating the original
+ var clonedNode = cloneNode(node);
+ var frag = document.createDocumentFragment();
+ var child;
+ /* eslint-disable no-cond-assign */
+ while (child = clonedNode.firstChild) {
+ /* eslint-enable no-cond-assign */
+ frag.appendChild(child);
+ }
+ trimNode(frag);
+ return frag;
+ }
+
+ // Test for the presence of the Safari template cloning bug
+ // https://bugs.webkit.org/showug.cgi?id=137755
+ var hasBrokenTemplate = (function () {
+ /* istanbul ignore else */
+ if (inBrowser) {
+ var a = document.createElement('div');
+ a.innerHTML = '<template>1</template>';
+ return !a.cloneNode(true).firstChild.innerHTML;
+ } else {
+ return false;
+ }
+ })();
+
+ // Test for IE10/11 textarea placeholder clone bug
+ var hasTextareaCloneBug = (function () {
+ /* istanbul ignore else */
+ if (inBrowser) {
+ var t = document.createElement('textarea');
+ t.placeholder = 't';
+ return t.cloneNode(true).value === 't';
+ } else {
+ return false;
+ }
+ })();
+
+ /**
+ * 1. Deal with Safari cloning nested <template> bug by
+ * manually cloning all template instances.
+ * 2. Deal with IE10/11 textarea placeholder bug by setting
+ * the correct value after cloning.
+ *
+ * @param {Element|DocumentFragment} node
+ * @return {Element|DocumentFragment}
+ */
+
+ function cloneNode(node) {
+ /* istanbul ignore if */
+ if (!node.querySelectorAll) {
+ return node.cloneNode();
+ }
+ var res = node.cloneNode(true);
+ var i, original, cloned;
+ /* istanbul ignore if */
+ if (hasBrokenTemplate) {
+ var tempClone = res;
+ if (isRealTemplate(node)) {
+ node = node.content;
+ tempClone = res.content;
+ }
+ original = node.querySelectorAll('template');
+ if (original.length) {
+ cloned = tempClone.querySelectorAll('template');
+ i = cloned.length;
+ while (i--) {
+ cloned[i].parentNode.replaceChild(cloneNode(original[i]), cloned[i]);
+ }
+ }
+ }
+ /* istanbul ignore if */
+ if (hasTextareaCloneBug) {
+ if (node.tagName === 'TEXTAREA') {
+ res.value = node.value;
+ } else {
+ original = node.querySelectorAll('textarea');
+ if (original.length) {
+ cloned = res.querySelectorAll('textarea');
+ i = cloned.length;
+ while (i--) {
+ cloned[i].value = original[i].value;
+ }
+ }
+ }
+ }
+ return res;
+ }
+
+ /**
+ * Process the template option and normalizes it into a
+ * a DocumentFragment that can be used as a partial or a
+ * instance template.
+ *
+ * @param {*} template
+ * Possible values include:
+ * - DocumentFragment object
+ * - Node object of type Template
+ * - id selector: '#some-template-id'
+ * - template string: '<div><span>{{msg}}</span></div>'
+ * @param {Boolean} shouldClone
+ * @param {Boolean} raw
+ * inline HTML interpolation. Do not check for id
+ * selector and keep whitespace in the string.
+ * @return {DocumentFragment|undefined}
+ */
+
+ function parseTemplate(template, shouldClone, raw) {
+ var node, frag;
+
+ // if the template is already a document fragment,
+ // do nothing
+ if (isFragment(template)) {
+ trimNode(template);
+ return shouldClone ? cloneNode(template) : template;
+ }
+
+ if (typeof template === 'string') {
+ // id selector
+ if (!raw && template.charAt(0) === '#') {
+ // id selector can be cached too
+ frag = idSelectorCache.get(template);
+ if (!frag) {
+ node = document.getElementById(template.slice(1));
+ if (node) {
+ frag = nodeToFragment(node);
+ // save selector to cache
+ idSelectorCache.put(template, frag);
+ }
+ }
+ } else {
+ // normal string template
+ frag = stringToFragment(template, raw);
+ }
+ } else if (template.nodeType) {
+ // a direct node
+ frag = nodeToFragment(template);
+ }
+
+ return frag && shouldClone ? cloneNode(frag) : frag;
+ }
+
+var template = Object.freeze({
+ cloneNode: cloneNode,
+ parseTemplate: parseTemplate
+ });
+
+ var html = {
+
+ bind: function bind() {
+ // a comment node means this is a binding for
+ // {{{ inline unescaped html }}}
+ if (this.el.nodeType === 8) {
+ // hold nodes
+ this.nodes = [];
+ // replace the placeholder with proper anchor
+ this.anchor = createAnchor('v-html');
+ replace(this.el, this.anchor);
+ }
+ },
+
+ update: function update(value) {
+ value = _toString(value);
+ if (this.nodes) {
+ this.swap(value);
+ } else {
+ this.el.innerHTML = value;
+ }
+ },
+
+ swap: function swap(value) {
+ // remove old nodes
+ var i = this.nodes.length;
+ while (i--) {
+ remove(this.nodes[i]);
+ }
+ // convert new value to a fragment
+ // do not attempt to retrieve from id selector
+ var frag = parseTemplate(value, true, true);
+ // save a reference to these nodes so we can remove later
+ this.nodes = toArray(frag.childNodes);
+ before(frag, this.anchor);
+ }
+ };
+
+ /**
+ * Abstraction for a partially-compiled fragment.
+ * Can optionally compile content with a child scope.
+ *
+ * @param {Function} linker
+ * @param {Vue} vm
+ * @param {DocumentFragment} frag
+ * @param {Vue} [host]
+ * @param {Object} [scope]
+ * @param {Fragment} [parentFrag]
+ */
+ function Fragment(linker, vm, frag, host, scope, parentFrag) {
+ this.children = [];
+ this.childFrags = [];
+ this.vm = vm;
+ this.scope = scope;
+ this.inserted = false;
+ this.parentFrag = parentFrag;
+ if (parentFrag) {
+ parentFrag.childFrags.push(this);
+ }
+ this.unlink = linker(vm, frag, host, scope, this);
+ var single = this.single = frag.childNodes.length === 1 &&
+ // do not go single mode if the only node is an anchor
+ !frag.childNodes[0].__v_anchor;
+ if (single) {
+ this.node = frag.childNodes[0];
+ this.before = singleBefore;
+ this.remove = singleRemove;
+ } else {
+ this.node = createAnchor('fragment-start');
+ this.end = createAnchor('fragment-end');
+ this.frag = frag;
+ prepend(this.node, frag);
+ frag.appendChild(this.end);
+ this.before = multiBefore;
+ this.remove = multiRemove;
+ }
+ this.node.__v_frag = this;
+ }
+
+ /**
+ * Call attach/detach for all components contained within
+ * this fragment. Also do so recursively for all child
+ * fragments.
+ *
+ * @param {Function} hook
+ */
+
+ Fragment.prototype.callHook = function (hook) {
+ var i, l;
+ for (i = 0, l = this.childFrags.length; i < l; i++) {
+ this.childFrags[i].callHook(hook);
+ }
+ for (i = 0, l = this.children.length; i < l; i++) {
+ hook(this.children[i]);
+ }
+ };
+
+ /**
+ * Insert fragment before target, single node version
+ *
+ * @param {Node} target
+ * @param {Boolean} withTransition
+ */
+
+ function singleBefore(target, withTransition) {
+ this.inserted = true;
+ var method = withTransition !== false ? beforeWithTransition : before;
+ method(this.node, target, this.vm);
+ if (inDoc(this.node)) {
+ this.callHook(attach);
+ }
+ }
+
+ /**
+ * Remove fragment, single node version
+ */
+
+ function singleRemove() {
+ this.inserted = false;
+ var shouldCallRemove = inDoc(this.node);
+ var self = this;
+ this.beforeRemove();
+ removeWithTransition(this.node, this.vm, function () {
+ if (shouldCallRemove) {
+ self.callHook(detach);
+ }
+ self.destroy();
+ });
+ }
+
+ /**
+ * Insert fragment before target, multi-nodes version
+ *
+ * @param {Node} target
+ * @param {Boolean} withTransition
+ */
+
+ function multiBefore(target, withTransition) {
+ this.inserted = true;
+ var vm = this.vm;
+ var method = withTransition !== false ? beforeWithTransition : before;
+ mapNodeRange(this.node, this.end, function (node) {
+ method(node, target, vm);
+ });
+ if (inDoc(this.node)) {
+ this.callHook(attach);
+ }
+ }
+
+ /**
+ * Remove fragment, multi-nodes version
+ */
+
+ function multiRemove() {
+ this.inserted = false;
+ var self = this;
+ var shouldCallRemove = inDoc(this.node);
+ this.beforeRemove();
+ removeNodeRange(this.node, this.end, this.vm, this.frag, function () {
+ if (shouldCallRemove) {
+ self.callHook(detach);
+ }
+ self.destroy();
+ });
+ }
+
+ /**
+ * Prepare the fragment for removal.
+ */
+
+ Fragment.prototype.beforeRemove = function () {
+ var i, l;
+ for (i = 0, l = this.childFrags.length; i < l; i++) {
+ // call the same method recursively on child
+ // fragments, depth-first
+ this.childFrags[i].beforeRemove(false);
+ }
+ for (i = 0, l = this.children.length; i < l; i++) {
+ // Call destroy for all contained instances,
+ // with remove:false and defer:true.
+ // Defer is necessary because we need to
+ // keep the children to call detach hooks
+ // on them.
+ this.children[i].$destroy(false, true);
+ }
+ var dirs = this.unlink.dirs;
+ for (i = 0, l = dirs.length; i < l; i++) {
+ // disable the watchers on all the directives
+ // so that the rendered content stays the same
+ // during removal.
+ dirs[i]._watcher && dirs[i]._watcher.teardown();
+ }
+ };
+
+ /**
+ * Destroy the fragment.
+ */
+
+ Fragment.prototype.destroy = function () {
+ if (this.parentFrag) {
+ this.parentFrag.childFrags.$remove(this);
+ }
+ this.node.__v_frag = null;
+ this.unlink();
+ };
+
+ /**
+ * Call attach hook for a Vue instance.
+ *
+ * @param {Vue} child
+ */
+
+ function attach(child) {
+ if (!child._isAttached && inDoc(child.$el)) {
+ child._callHook('attached');
+ }
+ }
+
+ /**
+ * Call detach hook for a Vue instance.
+ *
+ * @param {Vue} child
+ */
+
+ function detach(child) {
+ if (child._isAttached && !inDoc(child.$el)) {
+ child._callHook('detached');
+ }
+ }
+
+ var linkerCache = new Cache(5000);
+
+ /**
+ * A factory that can be used to create instances of a
+ * fragment. Caches the compiled linker if possible.
+ *
+ * @param {Vue} vm
+ * @param {Element|String} el
+ */
+ function FragmentFactory(vm, el) {
+ this.vm = vm;
+ var template;
+ var isString = typeof el === 'string';
+ if (isString || isTemplate(el) && !el.hasAttribute('v-if')) {
+ template = parseTemplate(el, true);
+ } else {
+ template = document.createDocumentFragment();
+ template.appendChild(el);
+ }
+ this.template = template;
+ // linker can be cached, but only for components
+ var linker;
+ var cid = vm.constructor.cid;
+ if (cid > 0) {
+ var cacheId = cid + (isString ? el : getOuterHTML(el));
+ linker = linkerCache.get(cacheId);
+ if (!linker) {
+ linker = compile(template, vm.$options, true);
+ linkerCache.put(cacheId, linker);
+ }
+ } else {
+ linker = compile(template, vm.$options, true);
+ }
+ this.linker = linker;
+ }
+
+ /**
+ * Create a fragment instance with given host and scope.
+ *
+ * @param {Vue} host
+ * @param {Object} scope
+ * @param {Fragment} parentFrag
+ */
+
+ FragmentFactory.prototype.create = function (host, scope, parentFrag) {
+ var frag = cloneNode(this.template);
+ return new Fragment(this.linker, this.vm, frag, host, scope, parentFrag);
+ };
+
+ var ON = 700;
+ var MODEL = 800;
+ var BIND = 850;
+ var TRANSITION = 1100;
+ var EL = 1500;
+ var COMPONENT = 1500;
+ var PARTIAL = 1750;
+ var IF = 2100;
+ var FOR = 2200;
+ var SLOT = 2300;
+
+ var uid$3 = 0;
+
+ var vFor = {
+
+ priority: FOR,
+ terminal: true,
+
+ params: ['track-by', 'stagger', 'enter-stagger', 'leave-stagger'],
+
+ bind: function bind() {
+ // support "item in/of items" syntax
+ var inMatch = this.expression.match(/(.*) (?:in|of) (.*)/);
+ if (inMatch) {
+ var itMatch = inMatch[1].match(/\((.*),(.*)\)/);
+ if (itMatch) {
+ this.iterator = itMatch[1].trim();
+ this.alias = itMatch[2].trim();
+ } else {
+ this.alias = inMatch[1].trim();
+ }
+ this.expression = inMatch[2];
+ }
+
+ if (!this.alias) {
+ 'development' !== 'production' && warn('Invalid v-for expression "' + this.descriptor.raw + '": ' + 'alias is required.', this.vm);
+ return;
+ }
+
+ // uid as a cache identifier
+ this.id = '__v-for__' + ++uid$3;
+
+ // check if this is an option list,
+ // so that we know if we need to update the <select>'s
+ // v-model when the option list has changed.
+ // because v-model has a lower priority than v-for,
+ // the v-model is not bound here yet, so we have to
+ // retrive it in the actual updateModel() function.
+ var tag = this.el.tagName;
+ this.isOption = (tag === 'OPTION' || tag === 'OPTGROUP') && this.el.parentNode.tagName === 'SELECT';
+
+ // setup anchor nodes
+ this.start = createAnchor('v-for-start');
+ this.end = createAnchor('v-for-end');
+ replace(this.el, this.end);
+ before(this.start, this.end);
+
+ // cache
+ this.cache = Object.create(null);
+
+ // fragment factory
+ this.factory = new FragmentFactory(this.vm, this.el);
+ },
+
+ update: function update(data) {
+ this.diff(data);
+ this.updateRef();
+ this.updateModel();
+ },
+
+ /**
+ * Diff, based on new data and old data, determine the
+ * minimum amount of DOM manipulations needed to make the
+ * DOM reflect the new data Array.
+ *
+ * The algorithm diffs the new data Array by storing a
+ * hidden reference to an owner vm instance on previously
+ * seen data. This allows us to achieve O(n) which is
+ * better than a levenshtein distance based algorithm,
+ * which is O(m * n).
+ *
+ * @param {Array} data
+ */
+
+ diff: function diff(data) {
+ // check if the Array was converted from an Object
+ var item = data[0];
+ var convertedFromObject = this.fromObject = isObject(item) && hasOwn(item, '$key') && hasOwn(item, '$value');
+
+ var trackByKey = this.params.trackBy;
+ var oldFrags = this.frags;
+ var frags = this.frags = new Array(data.length);
+ var alias = this.alias;
+ var iterator = this.iterator;
+ var start = this.start;
+ var end = this.end;
+ var inDocument = inDoc(start);
+ var init = !oldFrags;
+ var i, l, frag, key, value, primitive;
+
+ // First pass, go through the new Array and fill up
+ // the new frags array. If a piece of data has a cached
+ // instance for it, we reuse it. Otherwise build a new
+ // instance.
+ for (i = 0, l = data.length; i < l; i++) {
+ item = data[i];
+ key = convertedFromObject ? item.$key : null;
+ value = convertedFromObject ? item.$value : item;
+ primitive = !isObject(value);
+ frag = !init && this.getCachedFrag(value, i, key);
+ if (frag) {
+ // reusable fragment
+ frag.reused = true;
+ // update $index
+ frag.scope.$index = i;
+ // update $key
+ if (key) {
+ frag.scope.$key = key;
+ }
+ // update iterator
+ if (iterator) {
+ frag.scope[iterator] = key !== null ? key : i;
+ }
+ // update data for track-by, object repeat &
+ // primitive values.
+ if (trackByKey || convertedFromObject || primitive) {
+ withoutConversion(function () {
+ frag.scope[alias] = value;
+ });
+ }
+ } else {
+ // new isntance
+ frag = this.create(value, alias, i, key);
+ frag.fresh = !init;
+ }
+ frags[i] = frag;
+ if (init) {
+ frag.before(end);
+ }
+ }
+
+ // we're done for the initial render.
+ if (init) {
+ return;
+ }
+
+ // Second pass, go through the old fragments and
+ // destroy those who are not reused (and remove them
+ // from cache)
+ var removalIndex = 0;
+ var totalRemoved = oldFrags.length - frags.length;
+ // when removing a large number of fragments, watcher removal
+ // turns out to be a perf bottleneck, so we batch the watcher
+ // removals into a single filter call!
+ this.vm._vForRemoving = true;
+ for (i = 0, l = oldFrags.length; i < l; i++) {
+ frag = oldFrags[i];
+ if (!frag.reused) {
+ this.deleteCachedFrag(frag);
+ this.remove(frag, removalIndex++, totalRemoved, inDocument);
+ }
+ }
+ this.vm._vForRemoving = false;
+ if (removalIndex) {
+ this.vm._watchers = this.vm._watchers.filter(function (w) {
+ return w.active;
+ });
+ }
+
+ // Final pass, move/insert new fragments into the
+ // right place.
+ var targetPrev, prevEl, currentPrev;
+ var insertionIndex = 0;
+ for (i = 0, l = frags.length; i < l; i++) {
+ frag = frags[i];
+ // this is the frag that we should be after
+ targetPrev = frags[i - 1];
+ prevEl = targetPrev ? targetPrev.staggerCb ? targetPrev.staggerAnchor : targetPrev.end || targetPrev.node : start;
+ if (frag.reused && !frag.staggerCb) {
+ currentPrev = findPrevFrag(frag, start, this.id);
+ if (currentPrev !== targetPrev && (!currentPrev ||
+ // optimization for moving a single item.
+ // thanks to suggestions by @livoras in #1807
+ findPrevFrag(currentPrev, start, this.id) !== targetPrev)) {
+ this.move(frag, prevEl);
+ }
+ } else {
+ // new instance, or still in stagger.
+ // insert with updated stagger index.
+ this.insert(frag, insertionIndex++, prevEl, inDocument);
+ }
+ frag.reused = frag.fresh = false;
+ }
+ },
+
+ /**
+ * Create a new fragment instance.
+ *
+ * @param {*} value
+ * @param {String} alias
+ * @param {Number} index
+ * @param {String} [key]
+ * @return {Fragment}
+ */
+
+ create: function create(value, alias, index, key) {
+ var host = this._host;
+ // create iteration scope
+ var parentScope = this._scope || this.vm;
+ var scope = Object.create(parentScope);
+ // ref holder for the scope
+ scope.$refs = Object.create(parentScope.$refs);
+ scope.$els = Object.create(parentScope.$els);
+ // make sure point $parent to parent scope
+ scope.$parent = parentScope;
+ // for two-way binding on alias
+ scope.$forContext = this;
+ // define scope properties
+ // important: define the scope alias without forced conversion
+ // so that frozen data structures remain non-reactive.
+ withoutConversion(function () {
+ defineReactive(scope, alias, value);
+ });
+ defineReactive(scope, '$index', index);
+ if (key) {
+ defineReactive(scope, '$key', key);
+ } else if (scope.$key) {
+ // avoid accidental fallback
+ def(scope, '$key', null);
+ }
+ if (this.iterator) {
+ defineReactive(scope, this.iterator, key !== null ? key : index);
+ }
+ var frag = this.factory.create(host, scope, this._frag);
+ frag.forId = this.id;
+ this.cacheFrag(value, frag, index, key);
+ return frag;
+ },
+
+ /**
+ * Update the v-ref on owner vm.
+ */
+
+ updateRef: function updateRef() {
+ var ref = this.descriptor.ref;
+ if (!ref) return;
+ var hash = (this._scope || this.vm).$refs;
+ var refs;
+ if (!this.fromObject) {
+ refs = this.frags.map(findVmFromFrag);
+ } else {
+ refs = {};
+ this.frags.forEach(function (frag) {
+ refs[frag.scope.$key] = findVmFromFrag(frag);
+ });
+ }
+ hash[ref] = refs;
+ },
+
+ /**
+ * For option lists, update the containing v-model on
+ * parent <select>.
+ */
+
+ updateModel: function updateModel() {
+ if (this.isOption) {
+ var parent = this.start.parentNode;
+ var model = parent && parent.__v_model;
+ if (model) {
+ model.forceUpdate();
+ }
+ }
+ },
+
+ /**
+ * Insert a fragment. Handles staggering.
+ *
+ * @param {Fragment} frag
+ * @param {Number} index
+ * @param {Node} prevEl
+ * @param {Boolean} inDocument
+ */
+
+ insert: function insert(frag, index, prevEl, inDocument) {
+ if (frag.staggerCb) {
+ frag.staggerCb.cancel();
+ frag.staggerCb = null;
+ }
+ var staggerAmount = this.getStagger(frag, index, null, 'enter');
+ if (inDocument && staggerAmount) {
+ // create an anchor and insert it synchronously,
+ // so that we can resolve the correct order without
+ // worrying about some elements not inserted yet
+ var anchor = frag.staggerAnchor;
+ if (!anchor) {
+ anchor = frag.staggerAnchor = createAnchor('stagger-anchor');
+ anchor.__v_frag = frag;
+ }
+ after(anchor, prevEl);
+ var op = frag.staggerCb = cancellable(function () {
+ frag.staggerCb = null;
+ frag.before(anchor);
+ remove(anchor);
+ });
+ setTimeout(op, staggerAmount);
+ } else {
+ var target = prevEl.nextSibling;
+ /* istanbul ignore if */
+ if (!target) {
+ // reset end anchor position in case the position was messed up
+ // by an external drag-n-drop library.
+ after(this.end, prevEl);
+ target = this.end;
+ }
+ frag.before(target);
+ }
+ },
+
+ /**
+ * Remove a fragment. Handles staggering.
+ *
+ * @param {Fragment} frag
+ * @param {Number} index
+ * @param {Number} total
+ * @param {Boolean} inDocument
+ */
+
+ remove: function remove(frag, index, total, inDocument) {
+ if (frag.staggerCb) {
+ frag.staggerCb.cancel();
+ frag.staggerCb = null;
+ // it's not possible for the same frag to be removed
+ // twice, so if we have a pending stagger callback,
+ // it means this frag is queued for enter but removed
+ // before its transition started. Since it is already
+ // destroyed, we can just leave it in detached state.
+ return;
+ }
+ var staggerAmount = this.getStagger(frag, index, total, 'leave');
+ if (inDocument && staggerAmount) {
+ var op = frag.staggerCb = cancellable(function () {
+ frag.staggerCb = null;
+ frag.remove();
+ });
+ setTimeout(op, staggerAmount);
+ } else {
+ frag.remove();
+ }
+ },
+
+ /**
+ * Move a fragment to a new position.
+ * Force no transition.
+ *
+ * @param {Fragment} frag
+ * @param {Node} prevEl
+ */
+
+ move: function move(frag, prevEl) {
+ // fix a common issue with Sortable:
+ // if prevEl doesn't have nextSibling, this means it's
+ // been dragged after the end anchor. Just re-position
+ // the end anchor to the end of the container.
+ /* istanbul ignore if */
+ if (!prevEl.nextSibling) {
+ this.end.parentNode.appendChild(this.end);
+ }
+ frag.before(prevEl.nextSibling, false);
+ },
+
+ /**
+ * Cache a fragment using track-by or the object key.
+ *
+ * @param {*} value
+ * @param {Fragment} frag
+ * @param {Number} index
+ * @param {String} [key]
+ */
+
+ cacheFrag: function cacheFrag(value, frag, index, key) {
+ var trackByKey = this.params.trackBy;
+ var cache = this.cache;
+ var primitive = !isObject(value);
+ var id;
+ if (key || trackByKey || primitive) {
+ id = getTrackByKey(index, key, value, trackByKey);
+ if (!cache[id]) {
+ cache[id] = frag;
+ } else if (trackByKey !== '$index') {
+ 'development' !== 'production' && this.warnDuplicate(value);
+ }
+ } else {
+ id = this.id;
+ if (hasOwn(value, id)) {
+ if (value[id] === null) {
+ value[id] = frag;
+ } else {
+ 'development' !== 'production' && this.warnDuplicate(value);
+ }
+ } else if (Object.isExtensible(value)) {
+ def(value, id, frag);
+ } else if ('development' !== 'production') {
+ warn('Frozen v-for objects cannot be automatically tracked, make sure to ' + 'provide a track-by key.');
+ }
+ }
+ frag.raw = value;
+ },
+
+ /**
+ * Get a cached fragment from the value/index/key
+ *
+ * @param {*} value
+ * @param {Number} index
+ * @param {String} key
+ * @return {Fragment}
+ */
+
+ getCachedFrag: function getCachedFrag(value, index, key) {
+ var trackByKey = this.params.trackBy;
+ var primitive = !isObject(value);
+ var frag;
+ if (key || trackByKey || primitive) {
+ var id = getTrackByKey(index, key, value, trackByKey);
+ frag = this.cache[id];
+ } else {
+ frag = value[this.id];
+ }
+ if (frag && (frag.reused || frag.fresh)) {
+ 'development' !== 'production' && this.warnDuplicate(value);
+ }
+ return frag;
+ },
+
+ /**
+ * Delete a fragment from cache.
+ *
+ * @param {Fragment} frag
+ */
+
+ deleteCachedFrag: function deleteCachedFrag(frag) {
+ var value = frag.raw;
+ var trackByKey = this.params.trackBy;
+ var scope = frag.scope;
+ var index = scope.$index;
+ // fix #948: avoid accidentally fall through to
+ // a parent repeater which happens to have $key.
+ var key = hasOwn(scope, '$key') && scope.$key;
+ var primitive = !isObject(value);
+ if (trackByKey || key || primitive) {
+ var id = getTrackByKey(index, key, value, trackByKey);
+ this.cache[id] = null;
+ } else {
+ value[this.id] = null;
+ frag.raw = null;
+ }
+ },
+
+ /**
+ * Get the stagger amount for an insertion/removal.
+ *
+ * @param {Fragment} frag
+ * @param {Number} index
+ * @param {Number} total
+ * @param {String} type
+ */
+
+ getStagger: function getStagger(frag, index, total, type) {
+ type = type + 'Stagger';
+ var trans = frag.node.__v_trans;
+ var hooks = trans && trans.hooks;
+ var hook = hooks && (hooks[type] || hooks.stagger);
+ return hook ? hook.call(frag, index, total) : index * parseInt(this.params[type] || this.params.stagger, 10);
+ },
+
+ /**
+ * Pre-process the value before piping it through the
+ * filters. This is passed to and called by the watcher.
+ */
+
+ _preProcess: function _preProcess(value) {
+ // regardless of type, store the un-filtered raw value.
+ this.rawValue = value;
+ return value;
+ },
+
+ /**
+ * Post-process the value after it has been piped through
+ * the filters. This is passed to and called by the watcher.
+ *
+ * It is necessary for this to be called during the
+ * watcher's dependency collection phase because we want
+ * the v-for to update when the source Object is mutated.
+ */
+
+ _postProcess: function _postProcess(value) {
+ if (isArray(value)) {
+ return value;
+ } else if (isPlainObject(value)) {
+ // convert plain object to array.
+ var keys = Object.keys(value);
+ var i = keys.length;
+ var res = new Array(i);
+ var key;
+ while (i--) {
+ key = keys[i];
+ res[i] = {
+ $key: key,
+ $value: value[key]
+ };
+ }
+ return res;
+ } else {
+ if (typeof value === 'number' && !isNaN(value)) {
+ value = range(value);
+ }
+ return value || [];
+ }
+ },
+
+ unbind: function unbind() {
+ if (this.descriptor.ref) {
+ (this._scope || this.vm).$refs[this.descriptor.ref] = null;
+ }
+ if (this.frags) {
+ var i = this.frags.length;
+ var frag;
+ while (i--) {
+ frag = this.frags[i];
+ this.deleteCachedFrag(frag);
+ frag.destroy();
+ }
+ }
+ }
+ };
+
+ /**
+ * Helper to find the previous element that is a fragment
+ * anchor. This is necessary because a destroyed frag's
+ * element could still be lingering in the DOM before its
+ * leaving transition finishes, but its inserted flag
+ * should have been set to false so we can skip them.
+ *
+ * If this is a block repeat, we want to make sure we only
+ * return frag that is bound to this v-for. (see #929)
+ *
+ * @param {Fragment} frag
+ * @param {Comment|Text} anchor
+ * @param {String} id
+ * @return {Fragment}
+ */
+
+ function findPrevFrag(frag, anchor, id) {
+ var el = frag.node.previousSibling;
+ /* istanbul ignore if */
+ if (!el) return;
+ frag = el.__v_frag;
+ while ((!frag || frag.forId !== id || !frag.inserted) && el !== anchor) {
+ el = el.previousSibling;
+ /* istanbul ignore if */
+ if (!el) return;
+ frag = el.__v_frag;
+ }
+ return frag;
+ }
+
+ /**
+ * Find a vm from a fragment.
+ *
+ * @param {Fragment} frag
+ * @return {Vue|undefined}
+ */
+
+ function findVmFromFrag(frag) {
+ var node = frag.node;
+ // handle multi-node frag
+ if (frag.end) {
+ while (!node.__vue__ && node !== frag.end && node.nextSibling) {
+ node = node.nextSibling;
+ }
+ }
+ return node.__vue__;
+ }
+
+ /**
+ * Create a range array from given number.
+ *
+ * @param {Number} n
+ * @return {Array}
+ */
+
+ function range(n) {
+ var i = -1;
+ var ret = new Array(Math.floor(n));
+ while (++i < n) {
+ ret[i] = i;
+ }
+ return ret;
+ }
+
+ /**
+ * Get the track by key for an item.
+ *
+ * @param {Number} index
+ * @param {String} key
+ * @param {*} value
+ * @param {String} [trackByKey]
+ */
+
+ function getTrackByKey(index, key, value, trackByKey) {
+ return trackByKey ? trackByKey === '$index' ? index : trackByKey.charAt(0).match(/\w/) ? getPath(value, trackByKey) : value[trackByKey] : key || value;
+ }
+
+ if ('development' !== 'production') {
+ vFor.warnDuplicate = function (value) {
+ warn('Duplicate value found in v-for="' + this.descriptor.raw + '": ' + JSON.stringify(value) + '. Use track-by="$index" if ' + 'you are expecting duplicate values.', this.vm);
+ };
+ }
+
+ var vIf = {
+
+ priority: IF,
+ terminal: true,
+
+ bind: function bind() {
+ var el = this.el;
+ if (!el.__vue__) {
+ // check else block
+ var next = el.nextElementSibling;
+ if (next && getAttr(next, 'v-else') !== null) {
+ remove(next);
+ this.elseEl = next;
+ }
+ // check main block
+ this.anchor = createAnchor('v-if');
+ replace(el, this.anchor);
+ } else {
+ 'development' !== 'production' && warn('v-if="' + this.expression + '" cannot be ' + 'used on an instance root element.', this.vm);
+ this.invalid = true;
+ }
+ },
+
+ update: function update(value) {
+ if (this.invalid) return;
+ if (value) {
+ if (!this.frag) {
+ this.insert();
+ }
+ } else {
+ this.remove();
+ }
+ },
+
+ insert: function insert() {
+ if (this.elseFrag) {
+ this.elseFrag.remove();
+ this.elseFrag = null;
+ }
+ // lazy init factory
+ if (!this.factory) {
+ this.factory = new FragmentFactory(this.vm, this.el);
+ }
+ this.frag = this.factory.create(this._host, this._scope, this._frag);
+ this.frag.before(this.anchor);
+ },
+
+ remove: function remove() {
+ if (this.frag) {
+ this.frag.remove();
+ this.frag = null;
+ }
+ if (this.elseEl && !this.elseFrag) {
+ if (!this.elseFactory) {
+ this.elseFactory = new FragmentFactory(this.elseEl._context || this.vm, this.elseEl);
+ }
+ this.elseFrag = this.elseFactory.create(this._host, this._scope, this._frag);
+ this.elseFrag.before(this.anchor);
+ }
+ },
+
+ unbind: function unbind() {
+ if (this.frag) {
+ this.frag.destroy();
+ }
+ if (this.elseFrag) {
+ this.elseFrag.destroy();
+ }
+ }
+ };
+
+ var show = {
+
+ bind: function bind() {
+ // check else block
+ var next = this.el.nextElementSibling;
+ if (next && getAttr(next, 'v-else') !== null) {
+ this.elseEl = next;
+ }
+ },
+
+ update: function update(value) {
+ this.apply(this.el, value);
+ if (this.elseEl) {
+ this.apply(this.elseEl, !value);
+ }
+ },
+
+ apply: function apply(el, value) {
+ if (inDoc(el)) {
+ applyTransition(el, value ? 1 : -1, toggle, this.vm);
+ } else {
+ toggle();
+ }
+ function toggle() {
+ el.style.display = value ? '' : 'none';
+ }
+ }
+ };
+
+ var text$2 = {
+
+ bind: function bind() {
+ var self = this;
+ var el = this.el;
+ var isRange = el.type === 'range';
+ var lazy = this.params.lazy;
+ var number = this.params.number;
+ var debounce = this.params.debounce;
+
+ // handle composition events.
+ // http://blog.evanyou.me/2014/01/03/composition-event/
+ // skip this for Android because it handles composition
+ // events quite differently. Android doesn't trigger
+ // composition events for language input methods e.g.
+ // Chinese, but instead triggers them for spelling
+ // suggestions... (see Discussion/#162)
+ var composing = false;
+ if (!isAndroid && !isRange) {
+ this.on('compositionstart', function () {
+ composing = true;
+ });
+ this.on('compositionend', function () {
+ composing = false;
+ // in IE11 the "compositionend" event fires AFTER
+ // the "input" event, so the input handler is blocked
+ // at the end... have to call it here.
+ //
+ // #1327: in lazy mode this is unecessary.
+ if (!lazy) {
+ self.listener();
+ }
+ });
+ }
+
+ // prevent messing with the input when user is typing,
+ // and force update on blur.
+ this.focused = false;
+ if (!isRange && !lazy) {
+ this.on('focus', function () {
+ self.focused = true;
+ });
+ this.on('blur', function () {
+ self.focused = false;
+ // do not sync value after fragment removal (#2017)
+ if (!self._frag || self._frag.inserted) {
+ self.rawListener();
+ }
+ });
+ }
+
+ // Now attach the main listener
+ this.listener = this.rawListener = function () {
+ if (composing || !self._bound) {
+ return;
+ }
+ var val = number || isRange ? toNumber(el.value) : el.value;
+ self.set(val);
+ // force update on next tick to avoid lock & same value
+ // also only update when user is not typing
+ nextTick(function () {
+ if (self._bound && !self.focused) {
+ self.update(self._watcher.value);
+ }
+ });
+ };
+
+ // apply debounce
+ if (debounce) {
+ this.listener = _debounce(this.listener, debounce);
+ }
+
+ // Support jQuery events, since jQuery.trigger() doesn't
+ // trigger native events in some cases and some plugins
+ // rely on $.trigger()
+ //
+ // We want to make sure if a listener is attached using
+ // jQuery, it is also removed with jQuery, that's why
+ // we do the check for each directive instance and
+ // store that check result on itself. This also allows
+ // easier test coverage control by unsetting the global
+ // jQuery variable in tests.
+ this.hasjQuery = typeof jQuery === 'function';
+ if (this.hasjQuery) {
+ var method = jQuery.fn.on ? 'on' : 'bind';
+ jQuery(el)[method]('change', this.rawListener);
+ if (!lazy) {
+ jQuery(el)[method]('input', this.listener);
+ }
+ } else {
+ this.on('change', this.rawListener);
+ if (!lazy) {
+ this.on('input', this.listener);
+ }
+ }
+
+ // IE9 doesn't fire input event on backspace/del/cut
+ if (!lazy && isIE9) {
+ this.on('cut', function () {
+ nextTick(self.listener);
+ });
+ this.on('keyup', function (e) {
+ if (e.keyCode === 46 || e.keyCode === 8) {
+ self.listener();
+ }
+ });
+ }
+
+ // set initial value if present
+ if (el.hasAttribute('value') || el.tagName === 'TEXTAREA' && el.value.trim()) {
+ this.afterBind = this.listener;
+ }
+ },
+
+ update: function update(value) {
+ // #3029 only update when the value changes. This prevent
+ // browsers from overwriting values like selectionStart
+ value = _toString(value);
+ if (value !== this.el.value) this.el.value = value;
+ },
+
+ unbind: function unbind() {
+ var el = this.el;
+ if (this.hasjQuery) {
+ var method = jQuery.fn.off ? 'off' : 'unbind';
+ jQuery(el)[method]('change', this.listener);
+ jQuery(el)[method]('input', this.listener);
+ }
+ }
+ };
+
+ var radio = {
+
+ bind: function bind() {
+ var self = this;
+ var el = this.el;
+
+ this.getValue = function () {
+ // value overwrite via v-bind:value
+ if (el.hasOwnProperty('_value')) {
+ return el._value;
+ }
+ var val = el.value;
+ if (self.params.number) {
+ val = toNumber(val);
+ }
+ return val;
+ };
+
+ this.listener = function () {
+ self.set(self.getValue());
+ };
+ this.on('change', this.listener);
+
+ if (el.hasAttribute('checked')) {
+ this.afterBind = this.listener;
+ }
+ },
+
+ update: function update(value) {
+ this.el.checked = looseEqual(value, this.getValue());
+ }
+ };
+
+ var select = {
+
+ bind: function bind() {
+ var _this = this;
+
+ var self = this;
+ var el = this.el;
+
+ // method to force update DOM using latest value.
+ this.forceUpdate = function () {
+ if (self._watcher) {
+ self.update(self._watcher.get());
+ }
+ };
+
+ // check if this is a multiple select
+ var multiple = this.multiple = el.hasAttribute('multiple');
+
+ // attach listener
+ this.listener = function () {
+ var value = getValue(el, multiple);
+ value = self.params.number ? isArray(value) ? value.map(toNumber) : toNumber(value) : value;
+ self.set(value);
+ };
+ this.on('change', this.listener);
+
+ // if has initial value, set afterBind
+ var initValue = getValue(el, multiple, true);
+ if (multiple && initValue.length || !multiple && initValue !== null) {
+ this.afterBind = this.listener;
+ }
+
+ // All major browsers except Firefox resets
+ // selectedIndex with value -1 to 0 when the element
+ // is appended to a new parent, therefore we have to
+ // force a DOM update whenever that happens...
+ this.vm.$on('hook:attached', function () {
+ nextTick(_this.forceUpdate);
+ });
+ if (!inDoc(el)) {
+ nextTick(this.forceUpdate);
+ }
+ },
+
+ update: function update(value) {
+ var el = this.el;
+ el.selectedIndex = -1;
+ var multi = this.multiple && isArray(value);
+ var options = el.options;
+ var i = options.length;
+ var op, val;
+ while (i--) {
+ op = options[i];
+ val = op.hasOwnProperty('_value') ? op._value : op.value;
+ /* eslint-disable eqeqeq */
+ op.selected = multi ? indexOf$1(value, val) > -1 : looseEqual(value, val);
+ /* eslint-enable eqeqeq */
+ }
+ },
+
+ unbind: function unbind() {
+ /* istanbul ignore next */
+ this.vm.$off('hook:attached', this.forceUpdate);
+ }
+ };
+
+ /**
+ * Get select value
+ *
+ * @param {SelectElement} el
+ * @param {Boolean} multi
+ * @param {Boolean} init
+ * @return {Array|*}
+ */
+
+ function getValue(el, multi, init) {
+ var res = multi ? [] : null;
+ var op, val, selected;
+ for (var i = 0, l = el.options.length; i < l; i++) {
+ op = el.options[i];
+ selected = init ? op.hasAttribute('selected') : op.selected;
+ if (selected) {
+ val = op.hasOwnProperty('_value') ? op._value : op.value;
+ if (multi) {
+ res.push(val);
+ } else {
+ return val;
+ }
+ }
+ }
+ return res;
+ }
+
+ /**
+ * Native Array.indexOf uses strict equal, but in this
+ * case we need to match string/numbers with custom equal.
+ *
+ * @param {Array} arr
+ * @param {*} val
+ */
+
+ function indexOf$1(arr, val) {
+ var i = arr.length;
+ while (i--) {
+ if (looseEqual(arr[i], val)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ var checkbox = {
+
+ bind: function bind() {
+ var self = this;
+ var el = this.el;
+
+ this.getValue = function () {
+ return el.hasOwnProperty('_value') ? el._value : self.params.number ? toNumber(el.value) : el.value;
+ };
+
+ function getBooleanValue() {
+ var val = el.checked;
+ if (val && el.hasOwnProperty('_trueValue')) {
+ return el._trueValue;
+ }
+ if (!val && el.hasOwnProperty('_falseValue')) {
+ return el._falseValue;
+ }
+ return val;
+ }
+
+ this.listener = function () {
+ var model = self._watcher.value;
+ if (isArray(model)) {
+ var val = self.getValue();
+ if (el.checked) {
+ if (indexOf(model, val) < 0) {
+ model.push(val);
+ }
+ } else {
+ model.$remove(val);
+ }
+ } else {
+ self.set(getBooleanValue());
+ }
+ };
+
+ this.on('change', this.listener);
+ if (el.hasAttribute('checked')) {
+ this.afterBind = this.listener;
+ }
+ },
+
+ update: function update(value) {
+ var el = this.el;
+ if (isArray(value)) {
+ el.checked = indexOf(value, this.getValue()) > -1;
+ } else {
+ if (el.hasOwnProperty('_trueValue')) {
+ el.checked = looseEqual(value, el._trueValue);
+ } else {
+ el.checked = !!value;
+ }
+ }
+ }
+ };
+
+ var handlers = {
+ text: text$2,
+ radio: radio,
+ select: select,
+ checkbox: checkbox
+ };
+
+ var model = {
+
+ priority: MODEL,
+ twoWay: true,
+ handlers: handlers,
+ params: ['lazy', 'number', 'debounce'],
+
+ /**
+ * Possible elements:
+ * <select>
+ * <textarea>
+ * <input type="*">
+ * - text
+ * - checkbox
+ * - radio
+ * - number
+ */
+
+ bind: function bind() {
+ // friendly warning...
+ this.checkFilters();
+ if (this.hasRead && !this.hasWrite) {
+ 'development' !== 'production' && warn('It seems you are using a read-only filter with ' + 'v-model="' + this.descriptor.raw + '". ' + 'You might want to use a two-way filter to ensure correct behavior.', this.vm);
+ }
+ var el = this.el;
+ var tag = el.tagName;
+ var handler;
+ if (tag === 'INPUT') {
+ handler = handlers[el.type] || handlers.text;
+ } else if (tag === 'SELECT') {
+ handler = handlers.select;
+ } else if (tag === 'TEXTAREA') {
+ handler = handlers.text;
+ } else {
+ 'development' !== 'production' && warn('v-model does not support element type: ' + tag, this.vm);
+ return;
+ }
+ el.__v_model = this;
+ handler.bind.call(this);
+ this.update = handler.update;
+ this._unbind = handler.unbind;
+ },
+
+ /**
+ * Check read/write filter stats.
+ */
+
+ checkFilters: function checkFilters() {
+ var filters = this.filters;
+ if (!filters) return;
+ var i = filters.length;
+ while (i--) {
+ var filter = resolveAsset(this.vm.$options, 'filters', filters[i].name);
+ if (typeof filter === 'function' || filter.read) {
+ this.hasRead = true;
+ }
+ if (filter.write) {
+ this.hasWrite = true;
+ }
+ }
+ },
+
+ unbind: function unbind() {
+ this.el.__v_model = null;
+ this._unbind && this._unbind();
+ }
+ };
+
+ // keyCode aliases
+ var keyCodes = {
+ esc: 27,
+ tab: 9,
+ enter: 13,
+ space: 32,
+ 'delete': [8, 46],
+ up: 38,
+ left: 37,
+ right: 39,
+ down: 40
+ };
+
+ function keyFilter(handler, keys) {
+ var codes = keys.map(function (key) {
+ var charCode = key.charCodeAt(0);
+ if (charCode > 47 && charCode < 58) {
+ return parseInt(key, 10);
+ }
+ if (key.length === 1) {
+ charCode = key.toUpperCase().charCodeAt(0);
+ if (charCode > 64 && charCode < 91) {
+ return charCode;
+ }
+ }
+ return keyCodes[key];
+ });
+ codes = [].concat.apply([], codes);
+ return function keyHandler(e) {
+ if (codes.indexOf(e.keyCode) > -1) {
+ return handler.call(this, e);
+ }
+ };
+ }
+
+ function stopFilter(handler) {
+ return function stopHandler(e) {
+ e.stopPropagation();
+ return handler.call(this, e);
+ };
+ }
+
+ function preventFilter(handler) {
+ return function preventHandler(e) {
+ e.preventDefault();
+ return handler.call(this, e);
+ };
+ }
+
+ function selfFilter(handler) {
+ return function selfHandler(e) {
+ if (e.target === e.currentTarget) {
+ return handler.call(this, e);
+ }
+ };
+ }
+
+ var on$1 = {
+
+ priority: ON,
+ acceptStatement: true,
+ keyCodes: keyCodes,
+
+ bind: function bind() {
+ // deal with iframes
+ if (this.el.tagName === 'IFRAME' && this.arg !== 'load') {
+ var self = this;
+ this.iframeBind = function () {
+ on(self.el.contentWindow, self.arg, self.handler, self.modifiers.capture);
+ };
+ this.on('load', this.iframeBind);
+ }
+ },
+
+ update: function update(handler) {
+ // stub a noop for v-on with no value,
+ // e.g. @mousedown.prevent
+ if (!this.descriptor.raw) {
+ handler = function () {};
+ }
+
+ if (typeof handler !== 'function') {
+ 'development' !== 'production' && warn('v-on:' + this.arg + '="' + this.expression + '" expects a function value, ' + 'got ' + handler, this.vm);
+ return;
+ }
+
+ // apply modifiers
+ if (this.modifiers.stop) {
+ handler = stopFilter(handler);
+ }
+ if (this.modifiers.prevent) {
+ handler = preventFilter(handler);
+ }
+ if (this.modifiers.self) {
+ handler = selfFilter(handler);
+ }
+ // key filter
+ var keys = Object.keys(this.modifiers).filter(function (key) {
+ return key !== 'stop' && key !== 'prevent' && key !== 'self' && key !== 'capture';
+ });
+ if (keys.length) {
+ handler = keyFilter(handler, keys);
+ }
+
+ this.reset();
+ this.handler = handler;
+
+ if (this.iframeBind) {
+ this.iframeBind();
+ } else {
+ on(this.el, this.arg, this.handler, this.modifiers.capture);
+ }
+ },
+
+ reset: function reset() {
+ var el = this.iframeBind ? this.el.contentWindow : this.el;
+ if (this.handler) {
+ off(el, this.arg, this.handler);
+ }
+ },
+
+ unbind: function unbind() {
+ this.reset();
+ }
+ };
+
+ var prefixes = ['-webkit-', '-moz-', '-ms-'];
+ var camelPrefixes = ['Webkit', 'Moz', 'ms'];
+ var importantRE = /!important;?$/;
+ var propCache = Object.create(null);
+
+ var testEl = null;
+
+ var style = {
+
+ deep: true,
+
+ update: function update(value) {
+ if (typeof value === 'string') {
+ this.el.style.cssText = value;
+ } else if (isArray(value)) {
+ this.handleObject(value.reduce(extend, {}));
+ } else {
+ this.handleObject(value || {});
+ }
+ },
+
+ handleObject: function handleObject(value) {
+ // cache object styles so that only changed props
+ // are actually updated.
+ var cache = this.cache || (this.cache = {});
+ var name, val;
+ for (name in cache) {
+ if (!(name in value)) {
+ this.handleSingle(name, null);
+ delete cache[name];
+ }
+ }
+ for (name in value) {
+ val = value[name];
+ if (val !== cache[name]) {
+ cache[name] = val;
+ this.handleSingle(name, val);
+ }
+ }
+ },
+
+ handleSingle: function handleSingle(prop, value) {
+ prop = normalize(prop);
+ if (!prop) return; // unsupported prop
+ // cast possible numbers/booleans into strings
+ if (value != null) value += '';
+ if (value) {
+ var isImportant = importantRE.test(value) ? 'important' : '';
+ if (isImportant) {
+ /* istanbul ignore if */
+ if ('development' !== 'production') {
+ warn('It\'s probably a bad idea to use !important with inline rules. ' + 'This feature will be deprecated in a future version of Vue.');
+ }
+ value = value.replace(importantRE, '').trim();
+ this.el.style.setProperty(prop.kebab, value, isImportant);
+ } else {
+ this.el.style[prop.camel] = value;
+ }
+ } else {
+ this.el.style[prop.camel] = '';
+ }
+ }
+
+ };
+
+ /**
+ * Normalize a CSS property name.
+ * - cache result
+ * - auto prefix
+ * - camelCase -> dash-case
+ *
+ * @param {String} prop
+ * @return {String}
+ */
+
+ function normalize(prop) {
+ if (propCache[prop]) {
+ return propCache[prop];
+ }
+ var res = prefix(prop);
+ propCache[prop] = propCache[res] = res;
+ return res;
+ }
+
+ /**
+ * Auto detect the appropriate prefix for a CSS property.
+ * https://gist.github.com/paulirish/523692
+ *
+ * @param {String} prop
+ * @return {String}
+ */
+
+ function prefix(prop) {
+ prop = hyphenate(prop);
+ var camel = camelize(prop);
+ var upper = camel.charAt(0).toUpperCase() + camel.slice(1);
+ if (!testEl) {
+ testEl = document.createElement('div');
+ }
+ var i = prefixes.length;
+ var prefixed;
+ if (camel !== 'filter' && camel in testEl.style) {
+ return {
+ kebab: prop,
+ camel: camel
+ };
+ }
+ while (i--) {
+ prefixed = camelPrefixes[i] + upper;
+ if (prefixed in testEl.style) {
+ return {
+ kebab: prefixes[i] + prop,
+ camel: prefixed
+ };
+ }
+ }
+ }
+
+ // xlink
+ var xlinkNS = 'http://www.w3.org/1999/xlink';
+ var xlinkRE = /^xlink:/;
+
+ // check for attributes that prohibit interpolations
+ var disallowedInterpAttrRE = /^v-|^:|^@|^(?:is|transition|transition-mode|debounce|track-by|stagger|enter-stagger|leave-stagger)$/;
+ // these attributes should also set their corresponding properties
+ // because they only affect the initial state of the element
+ var attrWithPropsRE = /^(?:value|checked|selected|muted)$/;
+ // these attributes expect enumrated values of "true" or "false"
+ // but are not boolean attributes
+ var enumeratedAttrRE = /^(?:draggable|contenteditable|spellcheck)$/;
+
+ // these attributes should set a hidden property for
+ // binding v-model to object values
+ var modelProps = {
+ value: '_value',
+ 'true-value': '_trueValue',
+ 'false-value': '_falseValue'
+ };
+
+ var bind$1 = {
+
+ priority: BIND,
+
+ bind: function bind() {
+ var attr = this.arg;
+ var tag = this.el.tagName;
+ // should be deep watch on object mode
+ if (!attr) {
+ this.deep = true;
+ }
+ // handle interpolation bindings
+ var descriptor = this.descriptor;
+ var tokens = descriptor.interp;
+ if (tokens) {
+ // handle interpolations with one-time tokens
+ if (descriptor.hasOneTime) {
+ this.expression = tokensToExp(tokens, this._scope || this.vm);
+ }
+
+ // only allow binding on native attributes
+ if (disallowedInterpAttrRE.test(attr) || attr === 'name' && (tag === 'PARTIAL' || tag === 'SLOT')) {
+ 'development' !== 'production' && warn(attr + '="' + descriptor.raw + '": ' + 'attribute interpolation is not allowed in Vue.js ' + 'directives and special attributes.', this.vm);
+ this.el.removeAttribute(attr);
+ this.invalid = true;
+ }
+
+ /* istanbul ignore if */
+ if ('development' !== 'production') {
+ var raw = attr + '="' + descriptor.raw + '": ';
+ // warn src
+ if (attr === 'src') {
+ warn(raw + 'interpolation in "src" attribute will cause ' + 'a 404 request. Use v-bind:src instead.', this.vm);
+ }
+
+ // warn style
+ if (attr === 'style') {
+ warn(raw + 'interpolation in "style" attribute will cause ' + 'the attribute to be discarded in Internet Explorer. ' + 'Use v-bind:style instead.', this.vm);
+ }
+ }
+ }
+ },
+
+ update: function update(value) {
+ if (this.invalid) {
+ return;
+ }
+ var attr = this.arg;
+ if (this.arg) {
+ this.handleSingle(attr, value);
+ } else {
+ this.handleObject(value || {});
+ }
+ },
+
+ // share object handler with v-bind:class
+ handleObject: style.handleObject,
+
+ handleSingle: function handleSingle(attr, value) {
+ var el = this.el;
+ var interp = this.descriptor.interp;
+ if (this.modifiers.camel) {
+ attr = camelize(attr);
+ }
+ if (!interp && attrWithPropsRE.test(attr) && attr in el) {
+ var attrValue = attr === 'value' ? value == null // IE9 will set input.value to "null" for null...
+ ? '' : value : value;
+
+ if (el[attr] !== attrValue) {
+ el[attr] = attrValue;
+ }
+ }
+ // set model props
+ var modelProp = modelProps[attr];
+ if (!interp && modelProp) {
+ el[modelProp] = value;
+ // update v-model if present
+ var model = el.__v_model;
+ if (model) {
+ model.listener();
+ }
+ }
+ // do not set value attribute for textarea
+ if (attr === 'value' && el.tagName === 'TEXTAREA') {
+ el.removeAttribute(attr);
+ return;
+ }
+ // update attribute
+ if (enumeratedAttrRE.test(attr)) {
+ el.setAttribute(attr, value ? 'true' : 'false');
+ } else if (value != null && value !== false) {
+ if (attr === 'class') {
+ // handle edge case #1960:
+ // class interpolation should not overwrite Vue transition class
+ if (el.__v_trans) {
+ value += ' ' + el.__v_trans.id + '-transition';
+ }
+ setClass(el, value);
+ } else if (xlinkRE.test(attr)) {
+ el.setAttributeNS(xlinkNS, attr, value === true ? '' : value);
+ } else {
+ el.setAttribute(attr, value === true ? '' : value);
+ }
+ } else {
+ el.removeAttribute(attr);
+ }
+ }
+ };
+
+ var el = {
+
+ priority: EL,
+
+ bind: function bind() {
+ /* istanbul ignore if */
+ if (!this.arg) {
+ return;
+ }
+ var id = this.id = camelize(this.arg);
+ var refs = (this._scope || this.vm).$els;
+ if (hasOwn(refs, id)) {
+ refs[id] = this.el;
+ } else {
+ defineReactive(refs, id, this.el);
+ }
+ },
+
+ unbind: function unbind() {
+ var refs = (this._scope || this.vm).$els;
+ if (refs[this.id] === this.el) {
+ refs[this.id] = null;
+ }
+ }
+ };
+
+ var ref = {
+ bind: function bind() {
+ 'development' !== 'production' && warn('v-ref:' + this.arg + ' must be used on a child ' + 'component. Found on <' + this.el.tagName.toLowerCase() + '>.', this.vm);
+ }
+ };
+
+ var cloak = {
+ bind: function bind() {
+ var el = this.el;
+ this.vm.$once('pre-hook:compiled', function () {
+ el.removeAttribute('v-cloak');
+ });
+ }
+ };
+
+ // must export plain object
+ var directives = {
+ text: text$1,
+ html: html,
+ 'for': vFor,
+ 'if': vIf,
+ show: show,
+ model: model,
+ on: on$1,
+ bind: bind$1,
+ el: el,
+ ref: ref,
+ cloak: cloak
+ };
+
+ var vClass = {
+
+ deep: true,
+
+ update: function update(value) {
+ if (!value) {
+ this.cleanup();
+ } else if (typeof value === 'string') {
+ this.setClass(value.trim().split(/\s+/));
+ } else {
+ this.setClass(normalize$1(value));
+ }
+ },
+
+ setClass: function setClass(value) {
+ this.cleanup(value);
+ for (var i = 0, l = value.length; i < l; i++) {
+ var val = value[i];
+ if (val) {
+ apply(this.el, val, addClass);
+ }
+ }
+ this.prevKeys = value;
+ },
+
+ cleanup: function cleanup(value) {
+ var prevKeys = this.prevKeys;
+ if (!prevKeys) return;
+ var i = prevKeys.length;
+ while (i--) {
+ var key = prevKeys[i];
+ if (!value || value.indexOf(key) < 0) {
+ apply(this.el, key, removeClass);
+ }
+ }
+ }
+ };
+
+ /**
+ * Normalize objects and arrays (potentially containing objects)
+ * into array of strings.
+ *
+ * @param {Object|Array<String|Object>} value
+ * @return {Array<String>}
+ */
+
+ function normalize$1(value) {
+ var res = [];
+ if (isArray(value)) {
+ for (var i = 0, l = value.length; i < l; i++) {
+ var _key = value[i];
+ if (_key) {
+ if (typeof _key === 'string') {
+ res.push(_key);
+ } else {
+ for (var k in _key) {
+ if (_key[k]) res.push(k);
+ }
+ }
+ }
+ }
+ } else if (isObject(value)) {
+ for (var key in value) {
+ if (value[key]) res.push(key);
+ }
+ }
+ return res;
+ }
+
+ /**
+ * Add or remove a class/classes on an element
+ *
+ * @param {Element} el
+ * @param {String} key The class name. This may or may not
+ * contain a space character, in such a
+ * case we'll deal with multiple class
+ * names at once.
+ * @param {Function} fn
+ */
+
+ function apply(el, key, fn) {
+ key = key.trim();
+ if (key.indexOf(' ') === -1) {
+ fn(el, key);
+ return;
+ }
+ // The key contains one or more space characters.
+ // Since a class name doesn't accept such characters, we
+ // treat it as multiple classes.
+ var keys = key.split(/\s+/);
+ for (var i = 0, l = keys.length; i < l; i++) {
+ fn(el, keys[i]);
+ }
+ }
+
+ var component = {
+
+ priority: COMPONENT,
+
+ params: ['keep-alive', 'transition-mode', 'inline-template'],
+
+ /**
+ * Setup. Two possible usages:
+ *
+ * - static:
+ * <comp> or <div v-component="comp">
+ *
+ * - dynamic:
+ * <component :is="view">
+ */
+
+ bind: function bind() {
+ if (!this.el.__vue__) {
+ // keep-alive cache
+ this.keepAlive = this.params.keepAlive;
+ if (this.keepAlive) {
+ this.cache = {};
+ }
+ // check inline-template
+ if (this.params.inlineTemplate) {
+ // extract inline template as a DocumentFragment
+ this.inlineTemplate = extractContent(this.el, true);
+ }
+ // component resolution related state
+ this.pendingComponentCb = this.Component = null;
+ // transition related state
+ this.pendingRemovals = 0;
+ this.pendingRemovalCb = null;
+ // create a ref anchor
+ this.anchor = createAnchor('v-component');
+ replace(this.el, this.anchor);
+ // remove is attribute.
+ // this is removed during compilation, but because compilation is
+ // cached, when the component is used elsewhere this attribute
+ // will remain at link time.
+ this.el.removeAttribute('is');
+ this.el.removeAttribute(':is');
+ // remove ref, same as above
+ if (this.descriptor.ref) {
+ this.el.removeAttribute('v-ref:' + hyphenate(this.descriptor.ref));
+ }
+ // if static, build right now.
+ if (this.literal) {
+ this.setComponent(this.expression);
+ }
+ } else {
+ 'development' !== 'production' && warn('cannot mount component "' + this.expression + '" ' + 'on already mounted element: ' + this.el);
+ }
+ },
+
+ /**
+ * Public update, called by the watcher in the dynamic
+ * literal scenario, e.g. <component :is="view">
+ */
+
+ update: function update(value) {
+ if (!this.literal) {
+ this.setComponent(value);
+ }
+ },
+
+ /**
+ * Switch dynamic components. May resolve the component
+ * asynchronously, and perform transition based on
+ * specified transition mode. Accepts a few additional
+ * arguments specifically for vue-router.
+ *
+ * The callback is called when the full transition is
+ * finished.
+ *
+ * @param {String} value
+ * @param {Function} [cb]
+ */
+
+ setComponent: function setComponent(value, cb) {
+ this.invalidatePending();
+ if (!value) {
+ // just remove current
+ this.unbuild(true);
+ this.remove(this.childVM, cb);
+ this.childVM = null;
+ } else {
+ var self = this;
+ this.resolveComponent(value, function () {
+ self.mountComponent(cb);
+ });
+ }
+ },
+
+ /**
+ * Resolve the component constructor to use when creating
+ * the child vm.
+ *
+ * @param {String|Function} value
+ * @param {Function} cb
+ */
+
+ resolveComponent: function resolveComponent(value, cb) {
+ var self = this;
+ this.pendingComponentCb = cancellable(function (Component) {
+ self.ComponentName = Component.options.name || (typeof value === 'string' ? value : null);
+ self.Component = Component;
+ cb();
+ });
+ this.vm._resolveComponent(value, this.pendingComponentCb);
+ },
+
+ /**
+ * Create a new instance using the current constructor and
+ * replace the existing instance. This method doesn't care
+ * whether the new component and the old one are actually
+ * the same.
+ *
+ * @param {Function} [cb]
+ */
+
+ mountComponent: function mountComponent(cb) {
+ // actual mount
+ this.unbuild(true);
+ var self = this;
+ var activateHooks = this.Component.options.activate;
+ var cached = this.getCached();
+ var newComponent = this.build();
+ if (activateHooks && !cached) {
+ this.waitingFor = newComponent;
+ callActivateHooks(activateHooks, newComponent, function () {
+ if (self.waitingFor !== newComponent) {
+ return;
+ }
+ self.waitingFor = null;
+ self.transition(newComponent, cb);
+ });
+ } else {
+ // update ref for kept-alive component
+ if (cached) {
+ newComponent._updateRef();
+ }
+ this.transition(newComponent, cb);
+ }
+ },
+
+ /**
+ * When the component changes or unbinds before an async
+ * constructor is resolved, we need to invalidate its
+ * pending callback.
+ */
+
+ invalidatePending: function invalidatePending() {
+ if (this.pendingComponentCb) {
+ this.pendingComponentCb.cancel();
+ this.pendingComponentCb = null;
+ }
+ },
+
+ /**
+ * Instantiate/insert a new child vm.
+ * If keep alive and has cached instance, insert that
+ * instance; otherwise build a new one and cache it.
+ *
+ * @param {Object} [extraOptions]
+ * @return {Vue} - the created instance
+ */
+
+ build: function build(extraOptions) {
+ var cached = this.getCached();
+ if (cached) {
+ return cached;
+ }
+ if (this.Component) {
+ // default options
+ var options = {
+ name: this.ComponentName,
+ el: cloneNode(this.el),
+ template: this.inlineTemplate,
+ // make sure to add the child with correct parent
+ // if this is a transcluded component, its parent
+ // should be the transclusion host.
+ parent: this._host || this.vm,
+ // if no inline-template, then the compiled
+ // linker can be cached for better performance.
+ _linkerCachable: !this.inlineTemplate,
+ _ref: this.descriptor.ref,
+ _asComponent: true,
+ _isRouterView: this._isRouterView,
+ // if this is a transcluded component, context
+ // will be the common parent vm of this instance
+ // and its host.
+ _context: this.vm,
+ // if this is inside an inline v-for, the scope
+ // will be the intermediate scope created for this
+ // repeat fragment. this is used for linking props
+ // and container directives.
+ _scope: this._scope,
+ // pass in the owner fragment of this component.
+ // this is necessary so that the fragment can keep
+ // track of its contained components in order to
+ // call attach/detach hooks for them.
+ _frag: this._frag
+ };
+ // extra options
+ // in 1.0.0 this is used by vue-router only
+ /* istanbul ignore if */
+ if (extraOptions) {
+ extend(options, extraOptions);
+ }
+ var child = new this.Component(options);
+ if (this.keepAlive) {
+ this.cache[this.Component.cid] = child;
+ }
+ /* istanbul ignore if */
+ if ('development' !== 'production' && this.el.hasAttribute('transition') && child._isFragment) {
+ warn('Transitions will not work on a fragment instance. ' + 'Template: ' + child.$options.template, child);
+ }
+ return child;
+ }
+ },
+
+ /**
+ * Try to get a cached instance of the current component.
+ *
+ * @return {Vue|undefined}
+ */
+
+ getCached: function getCached() {
+ return this.keepAlive && this.cache[this.Component.cid];
+ },
+
+ /**
+ * Teardown the current child, but defers cleanup so
+ * that we can separate the destroy and removal steps.
+ *
+ * @param {Boolean} defer
+ */
+
+ unbuild: function unbuild(defer) {
+ if (this.waitingFor) {
+ if (!this.keepAlive) {
+ this.waitingFor.$destroy();
+ }
+ this.waitingFor = null;
+ }
+ var child = this.childVM;
+ if (!child || this.keepAlive) {
+ if (child) {
+ // remove ref
+ child._inactive = true;
+ child._updateRef(true);
+ }
+ return;
+ }
+ // the sole purpose of `deferCleanup` is so that we can
+ // "deactivate" the vm right now and perform DOM removal
+ // later.
+ child.$destroy(false, defer);
+ },
+
+ /**
+ * Remove current destroyed child and manually do
+ * the cleanup after removal.
+ *
+ * @param {Function} cb
+ */
+
+ remove: function remove(child, cb) {
+ var keepAlive = this.keepAlive;
+ if (child) {
+ // we may have a component switch when a previous
+ // component is still being transitioned out.
+ // we want to trigger only one lastest insertion cb
+ // when the existing transition finishes. (#1119)
+ this.pendingRemovals++;
+ this.pendingRemovalCb = cb;
+ var self = this;
+ child.$remove(function () {
+ self.pendingRemovals--;
+ if (!keepAlive) child._cleanup();
+ if (!self.pendingRemovals && self.pendingRemovalCb) {
+ self.pendingRemovalCb();
+ self.pendingRemovalCb = null;
+ }
+ });
+ } else if (cb) {
+ cb();
+ }
+ },
+
+ /**
+ * Actually swap the components, depending on the
+ * transition mode. Defaults to simultaneous.
+ *
+ * @param {Vue} target
+ * @param {Function} [cb]
+ */
+
+ transition: function transition(target, cb) {
+ var self = this;
+ var current = this.childVM;
+ // for devtool inspection
+ if (current) current._inactive = true;
+ target._inactive = false;
+ this.childVM = target;
+ switch (self.params.transitionMode) {
+ case 'in-out':
+ target.$before(self.anchor, function () {
+ self.remove(current, cb);
+ });
+ break;
+ case 'out-in':
+ self.remove(current, function () {
+ target.$before(self.anchor, cb);
+ });
+ break;
+ default:
+ self.remove(current);
+ target.$before(self.anchor, cb);
+ }
+ },
+
+ /**
+ * Unbind.
+ */
+
+ unbind: function unbind() {
+ this.invalidatePending();
+ // Do not defer cleanup when unbinding
+ this.unbuild();
+ // destroy all keep-alive cached instances
+ if (this.cache) {
+ for (var key in this.cache) {
+ this.cache[key].$destroy();
+ }
+ this.cache = null;
+ }
+ }
+ };
+
+ /**
+ * Call activate hooks in order (asynchronous)
+ *
+ * @param {Array} hooks
+ * @param {Vue} vm
+ * @param {Function} cb
+ */
+
+ function callActivateHooks(hooks, vm, cb) {
+ var total = hooks.length;
+ var called = 0;
+ hooks[0].call(vm, next);
+ function next() {
+ if (++called >= total) {
+ cb();
+ } else {
+ hooks[called].call(vm, next);
+ }
+ }
+ }
+
+ var propBindingModes = config._propBindingModes;
+ var empty = {};
+
+ // regexes
+ var identRE$1 = /^[$_a-zA-Z]+[\w$]*$/;
+ var settablePathRE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\[[^\[\]]+\])*$/;
+
+ /**
+ * Compile props on a root element and return
+ * a props link function.
+ *
+ * @param {Element|DocumentFragment} el
+ * @param {Array} propOptions
+ * @param {Vue} vm
+ * @return {Function} propsLinkFn
+ */
+
+ function compileProps(el, propOptions, vm) {
+ var props = [];
+ var names = Object.keys(propOptions);
+ var i = names.length;
+ var options, name, attr, value, path, parsed, prop;
+ while (i--) {
+ name = names[i];
+ options = propOptions[name] || empty;
+
+ if ('development' !== 'production' && name === '$data') {
+ warn('Do not use $data as prop.', vm);
+ continue;
+ }
+
+ // props could contain dashes, which will be
+ // interpreted as minus calculations by the parser
+ // so we need to camelize the path here
+ path = camelize(name);
+ if (!identRE$1.test(path)) {
+ 'development' !== 'production' && warn('Invalid prop key: "' + name + '". Prop keys ' + 'must be valid identifiers.', vm);
+ continue;
+ }
+
+ prop = {
+ name: name,
+ path: path,
+ options: options,
+ mode: propBindingModes.ONE_WAY,
+ raw: null
+ };
+
+ attr = hyphenate(name);
+ // first check dynamic version
+ if ((value = getBindAttr(el, attr)) === null) {
+ if ((value = getBindAttr(el, attr + '.sync')) !== null) {
+ prop.mode = propBindingModes.TWO_WAY;
+ } else if ((value = getBindAttr(el, attr + '.once')) !== null) {
+ prop.mode = propBindingModes.ONE_TIME;
+ }
+ }
+ if (value !== null) {
+ // has dynamic binding!
+ prop.raw = value;
+ parsed = parseDirective(value);
+ value = parsed.expression;
+ prop.filters = parsed.filters;
+ // check binding type
+ if (isLiteral(value) && !parsed.filters) {
+ // for expressions containing literal numbers and
+ // booleans, there's no need to setup a prop binding,
+ // so we can optimize them as a one-time set.
+ prop.optimizedLiteral = true;
+ } else {
+ prop.dynamic = true;
+ // check non-settable path for two-way bindings
+ if ('development' !== 'production' && prop.mode === propBindingModes.TWO_WAY && !settablePathRE.test(value)) {
+ prop.mode = propBindingModes.ONE_WAY;
+ warn('Cannot bind two-way prop with non-settable ' + 'parent path: ' + value, vm);
+ }
+ }
+ prop.parentPath = value;
+
+ // warn required two-way
+ if ('development' !== 'production' && options.twoWay && prop.mode !== propBindingModes.TWO_WAY) {
+ warn('Prop "' + name + '" expects a two-way binding type.', vm);
+ }
+ } else if ((value = getAttr(el, attr)) !== null) {
+ // has literal binding!
+ prop.raw = value;
+ } else if ('development' !== 'production') {
+ // check possible camelCase prop usage
+ var lowerCaseName = path.toLowerCase();
+ value = /[A-Z\-]/.test(name) && (el.getAttribute(lowerCaseName) || el.getAttribute(':' + lowerCaseName) || el.getAttribute('v-bind:' + lowerCaseName) || el.getAttribute(':' + lowerCaseName + '.once') || el.getAttribute('v-bind:' + lowerCaseName + '.once') || el.getAttribute(':' + lowerCaseName + '.sync') || el.getAttribute('v-bind:' + lowerCaseName + '.sync'));
+ if (value) {
+ warn('Possible usage error for prop `' + lowerCaseName + '` - ' + 'did you mean `' + attr + '`? HTML is case-insensitive, remember to use ' + 'kebab-case for props in templates.', vm);
+ } else if (options.required) {
+ // warn missing required
+ warn('Missing required prop: ' + name, vm);
+ }
+ }
+ // push prop
+ props.push(prop);
+ }
+ return makePropsLinkFn(props);
+ }
+
+ /**
+ * Build a function that applies props to a vm.
+ *
+ * @param {Array} props
+ * @return {Function} propsLinkFn
+ */
+
+ function makePropsLinkFn(props) {
+ return function propsLinkFn(vm, scope) {
+ // store resolved props info
+ vm._props = {};
+ var inlineProps = vm.$options.propsData;
+ var i = props.length;
+ var prop, path, options, value, raw;
+ while (i--) {
+ prop = props[i];
+ raw = prop.raw;
+ path = prop.path;
+ options = prop.options;
+ vm._props[path] = prop;
+ if (inlineProps && hasOwn(inlineProps, path)) {
+ initProp(vm, prop, inlineProps[path]);
+ }if (raw === null) {
+ // initialize absent prop
+ initProp(vm, prop, undefined);
+ } else if (prop.dynamic) {
+ // dynamic prop
+ if (prop.mode === propBindingModes.ONE_TIME) {
+ // one time binding
+ value = (scope || vm._context || vm).$get(prop.parentPath);
+ initProp(vm, prop, value);
+ } else {
+ if (vm._context) {
+ // dynamic binding
+ vm._bindDir({
+ name: 'prop',
+ def: propDef,
+ prop: prop
+ }, null, null, scope); // el, host, scope
+ } else {
+ // root instance
+ initProp(vm, prop, vm.$get(prop.parentPath));
+ }
+ }
+ } else if (prop.optimizedLiteral) {
+ // optimized literal, cast it and just set once
+ var stripped = stripQuotes(raw);
+ value = stripped === raw ? toBoolean(toNumber(raw)) : stripped;
+ initProp(vm, prop, value);
+ } else {
+ // string literal, but we need to cater for
+ // Boolean props with no value, or with same
+ // literal value (e.g. disabled="disabled")
+ // see https://github.com/vuejs/vue-loader/issues/182
+ value = options.type === Boolean && (raw === '' || raw === hyphenate(prop.name)) ? true : raw;
+ initProp(vm, prop, value);
+ }
+ }
+ };
+ }
+
+ /**
+ * Process a prop with a rawValue, applying necessary coersions,
+ * default values & assertions and call the given callback with
+ * processed value.
+ *
+ * @param {Vue} vm
+ * @param {Object} prop
+ * @param {*} rawValue
+ * @param {Function} fn
+ */
+
+ function processPropValue(vm, prop, rawValue, fn) {
+ var isSimple = prop.dynamic && isSimplePath(prop.parentPath);
+ var value = rawValue;
+ if (value === undefined) {
+ value = getPropDefaultValue(vm, prop);
+ }
+ value = coerceProp(prop, value, vm);
+ var coerced = value !== rawValue;
+ if (!assertProp(prop, value, vm)) {
+ value = undefined;
+ }
+ if (isSimple && !coerced) {
+ withoutConversion(function () {
+ fn(value);
+ });
+ } else {
+ fn(value);
+ }
+ }
+
+ /**
+ * Set a prop's initial value on a vm and its data object.
+ *
+ * @param {Vue} vm
+ * @param {Object} prop
+ * @param {*} value
+ */
+
+ function initProp(vm, prop, value) {
+ processPropValue(vm, prop, value, function (value) {
+ defineReactive(vm, prop.path, value);
+ });
+ }
+
+ /**
+ * Update a prop's value on a vm.
+ *
+ * @param {Vue} vm
+ * @param {Object} prop
+ * @param {*} value
+ */
+
+ function updateProp(vm, prop, value) {
+ processPropValue(vm, prop, value, function (value) {
+ vm[prop.path] = value;
+ });
+ }
+
+ /**
+ * Get the default value of a prop.
+ *
+ * @param {Vue} vm
+ * @param {Object} prop
+ * @return {*}
+ */
+
+ function getPropDefaultValue(vm, prop) {
+ // no default, return undefined
+ var options = prop.options;
+ if (!hasOwn(options, 'default')) {
+ // absent boolean value defaults to false
+ return options.type === Boolean ? false : undefined;
+ }
+ var def = options['default'];
+ // warn against non-factory defaults for Object & Array
+ if (isObject(def)) {
+ 'development' !== 'production' && warn('Invalid default value for prop "' + prop.name + '": ' + 'Props with type Object/Array must use a factory function ' + 'to return the default value.', vm);
+ }
+ // call factory function for non-Function types
+ return typeof def === 'function' && options.type !== Function ? def.call(vm) : def;
+ }
+
+ /**
+ * Assert whether a prop is valid.
+ *
+ * @param {Object} prop
+ * @param {*} value
+ * @param {Vue} vm
+ */
+
+ function assertProp(prop, value, vm) {
+ if (!prop.options.required && ( // non-required
+ prop.raw === null || // abscent
+ value == null) // null or undefined
+ ) {
+ return true;
+ }
+ var options = prop.options;
+ var type = options.type;
+ var valid = !type;
+ var expectedTypes = [];
+ if (type) {
+ if (!isArray(type)) {
+ type = [type];
+ }
+ for (var i = 0; i < type.length && !valid; i++) {
+ var assertedType = assertType(value, type[i]);
+ expectedTypes.push(assertedType.expectedType);
+ valid = assertedType.valid;
+ }
+ }
+ if (!valid) {
+ if ('development' !== 'production') {
+ warn('Invalid prop: type check failed for prop "' + prop.name + '".' + ' Expected ' + expectedTypes.map(formatType).join(', ') + ', got ' + formatValue(value) + '.', vm);
+ }
+ return false;
+ }
+ var validator = options.validator;
+ if (validator) {
+ if (!validator(value)) {
+ 'development' !== 'production' && warn('Invalid prop: custom validator check failed for prop "' + prop.name + '".', vm);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Force parsing value with coerce option.
+ *
+ * @param {*} value
+ * @param {Object} options
+ * @return {*}
+ */
+
+ function coerceProp(prop, value, vm) {
+ var coerce = prop.options.coerce;
+ if (!coerce) {
+ return value;
+ }
+ if (typeof coerce === 'function') {
+ return coerce(value);
+ } else {
+ 'development' !== 'production' && warn('Invalid coerce for prop "' + prop.name + '": expected function, got ' + typeof coerce + '.', vm);
+ return value;
+ }
+ }
+
+ /**
+ * Assert the type of a value
+ *
+ * @param {*} value
+ * @param {Function} type
+ * @return {Object}
+ */
+
+ function assertType(value, type) {
+ var valid;
+ var expectedType;
+ if (type === String) {
+ expectedType = 'string';
+ valid = typeof value === expectedType;
+ } else if (type === Number) {
+ expectedType = 'number';
+ valid = typeof value === expectedType;
+ } else if (type === Boolean) {
+ expectedType = 'boolean';
+ valid = typeof value === expectedType;
+ } else if (type === Function) {
+ expectedType = 'function';
+ valid = typeof value === expectedType;
+ } else if (type === Object) {
+ expectedType = 'object';
+ valid = isPlainObject(value);
+ } else if (type === Array) {
+ expectedType = 'array';
+ valid = isArray(value);
+ } else {
+ valid = value instanceof type;
+ }
+ return {
+ valid: valid,
+ expectedType: expectedType
+ };
+ }
+
+ /**
+ * Format type for output
+ *
+ * @param {String} type
+ * @return {String}
+ */
+
+ function formatType(type) {
+ return type ? type.charAt(0).toUpperCase() + type.slice(1) : 'custom type';
+ }
+
+ /**
+ * Format value
+ *
+ * @param {*} value
+ * @return {String}
+ */
+
+ function formatValue(val) {
+ return Object.prototype.toString.call(val).slice(8, -1);
+ }
+
+ var bindingModes = config._propBindingModes;
+
+ var propDef = {
+
+ bind: function bind() {
+ var child = this.vm;
+ var parent = child._context;
+ // passed in from compiler directly
+ var prop = this.descriptor.prop;
+ var childKey = prop.path;
+ var parentKey = prop.parentPath;
+ var twoWay = prop.mode === bindingModes.TWO_WAY;
+
+ var parentWatcher = this.parentWatcher = new Watcher(parent, parentKey, function (val) {
+ updateProp(child, prop, val);
+ }, {
+ twoWay: twoWay,
+ filters: prop.filters,
+ // important: props need to be observed on the
+ // v-for scope if present
+ scope: this._scope
+ });
+
+ // set the child initial value.
+ initProp(child, prop, parentWatcher.value);
+
+ // setup two-way binding
+ if (twoWay) {
+ // important: defer the child watcher creation until
+ // the created hook (after data observation)
+ var self = this;
+ child.$once('pre-hook:created', function () {
+ self.childWatcher = new Watcher(child, childKey, function (val) {
+ parentWatcher.set(val);
+ }, {
+ // ensure sync upward before parent sync down.
+ // this is necessary in cases e.g. the child
+ // mutates a prop array, then replaces it. (#1683)
+ sync: true
+ });
+ });
+ }
+ },
+
+ unbind: function unbind() {
+ this.parentWatcher.teardown();
+ if (this.childWatcher) {
+ this.childWatcher.teardown();
+ }
+ }
+ };
+
+ var queue$1 = [];
+ var queued = false;
+
+ /**
+ * Push a job into the queue.
+ *
+ * @param {Function} job
+ */
+
+ function pushJob(job) {
+ queue$1.push(job);
+ if (!queued) {
+ queued = true;
+ nextTick(flush);
+ }
+ }
+
+ /**
+ * Flush the queue, and do one forced reflow before
+ * triggering transitions.
+ */
+
+ function flush() {
+ // Force layout
+ var f = document.documentElement.offsetHeight;
+ for (var i = 0; i < queue$1.length; i++) {
+ queue$1[i]();
+ }
+ queue$1 = [];
+ queued = false;
+ // dummy return, so js linters don't complain about
+ // unused variable f
+ return f;
+ }
+
+ var TYPE_TRANSITION = 'transition';
+ var TYPE_ANIMATION = 'animation';
+ var transDurationProp = transitionProp + 'Duration';
+ var animDurationProp = animationProp + 'Duration';
+
+ /**
+ * If a just-entered element is applied the
+ * leave class while its enter transition hasn't started yet,
+ * and the transitioned property has the same value for both
+ * enter/leave, then the leave transition will be skipped and
+ * the transitionend event never fires. This function ensures
+ * its callback to be called after a transition has started
+ * by waiting for double raf.
+ *
+ * It falls back to setTimeout on devices that support CSS
+ * transitions but not raf (e.g. Android 4.2 browser) - since
+ * these environments are usually slow, we are giving it a
+ * relatively large timeout.
+ */
+
+ var raf = inBrowser && window.requestAnimationFrame;
+ var waitForTransitionStart = raf
+ /* istanbul ignore next */
+ ? function (fn) {
+ raf(function () {
+ raf(fn);
+ });
+ } : function (fn) {
+ setTimeout(fn, 50);
+ };
+
+ /**
+ * A Transition object that encapsulates the state and logic
+ * of the transition.
+ *
+ * @param {Element} el
+ * @param {String} id
+ * @param {Object} hooks
+ * @param {Vue} vm
+ */
+ function Transition(el, id, hooks, vm) {
+ this.id = id;
+ this.el = el;
+ this.enterClass = hooks && hooks.enterClass || id + '-enter';
+ this.leaveClass = hooks && hooks.leaveClass || id + '-leave';
+ this.hooks = hooks;
+ this.vm = vm;
+ // async state
+ this.pendingCssEvent = this.pendingCssCb = this.cancel = this.pendingJsCb = this.op = this.cb = null;
+ this.justEntered = false;
+ this.entered = this.left = false;
+ this.typeCache = {};
+ // check css transition type
+ this.type = hooks && hooks.type;
+ /* istanbul ignore if */
+ if ('development' !== 'production') {
+ if (this.type && this.type !== TYPE_TRANSITION && this.type !== TYPE_ANIMATION) {
+ warn('invalid CSS transition type for transition="' + this.id + '": ' + this.type, vm);
+ }
+ }
+ // bind
+ var self = this;['enterNextTick', 'enterDone', 'leaveNextTick', 'leaveDone'].forEach(function (m) {
+ self[m] = bind(self[m], self);
+ });
+ }
+
+ var p$1 = Transition.prototype;
+
+ /**
+ * Start an entering transition.
+ *
+ * 1. enter transition triggered
+ * 2. call beforeEnter hook
+ * 3. add enter class
+ * 4. insert/show element
+ * 5. call enter hook (with possible explicit js callback)
+ * 6. reflow
+ * 7. based on transition type:
+ * - transition:
+ * remove class now, wait for transitionend,
+ * then done if there's no explicit js callback.
+ * - animation:
+ * wait for animationend, remove class,
+ * then done if there's no explicit js callback.
+ * - no css transition:
+ * done now if there's no explicit js callback.
+ * 8. wait for either done or js callback, then call
+ * afterEnter hook.
+ *
+ * @param {Function} op - insert/show the element
+ * @param {Function} [cb]
+ */
+
+ p$1.enter = function (op, cb) {
+ this.cancelPending();
+ this.callHook('beforeEnter');
+ this.cb = cb;
+ addClass(this.el, this.enterClass);
+ op();
+ this.entered = false;
+ this.callHookWithCb('enter');
+ if (this.entered) {
+ return; // user called done synchronously.
+ }
+ this.cancel = this.hooks && this.hooks.enterCancelled;
+ pushJob(this.enterNextTick);
+ };
+
+ /**
+ * The "nextTick" phase of an entering transition, which is
+ * to be pushed into a queue and executed after a reflow so
+ * that removing the class can trigger a CSS transition.
+ */
+
+ p$1.enterNextTick = function () {
+ var _this = this;
+
+ // prevent transition skipping
+ this.justEntered = true;
+ waitForTransitionStart(function () {
+ _this.justEntered = false;
+ });
+ var enterDone = this.enterDone;
+ var type = this.getCssTransitionType(this.enterClass);
+ if (!this.pendingJsCb) {
+ if (type === TYPE_TRANSITION) {
+ // trigger transition by removing enter class now
+ removeClass(this.el, this.enterClass);
+ this.setupCssCb(transitionEndEvent, enterDone);
+ } else if (type === TYPE_ANIMATION) {
+ this.setupCssCb(animationEndEvent, enterDone);
+ } else {
+ enterDone();
+ }
+ } else if (type === TYPE_TRANSITION) {
+ removeClass(this.el, this.enterClass);
+ }
+ };
+
+ /**
+ * The "cleanup" phase of an entering transition.
+ */
+
+ p$1.enterDone = function () {
+ this.entered = true;
+ this.cancel = this.pendingJsCb = null;
+ removeClass(this.el, this.enterClass);
+ this.callHook('afterEnter');
+ if (this.cb) this.cb();
+ };
+
+ /**
+ * Start a leaving transition.
+ *
+ * 1. leave transition triggered.
+ * 2. call beforeLeave hook
+ * 3. add leave class (trigger css transition)
+ * 4. call leave hook (with possible explicit js callback)
+ * 5. reflow if no explicit js callback is provided
+ * 6. based on transition type:
+ * - transition or animation:
+ * wait for end event, remove class, then done if
+ * there's no explicit js callback.
+ * - no css transition:
+ * done if there's no explicit js callback.
+ * 7. wait for either done or js callback, then call
+ * afterLeave hook.
+ *
+ * @param {Function} op - remove/hide the element
+ * @param {Function} [cb]
+ */
+
+ p$1.leave = function (op, cb) {
+ this.cancelPending();
+ this.callHook('beforeLeave');
+ this.op = op;
+ this.cb = cb;
+ addClass(this.el, this.leaveClass);
+ this.left = false;
+ this.callHookWithCb('leave');
+ if (this.left) {
+ return; // user called done synchronously.
+ }
+ this.cancel = this.hooks && this.hooks.leaveCancelled;
+ // only need to handle leaveDone if
+ // 1. the transition is already done (synchronously called
+ // by the user, which causes this.op set to null)
+ // 2. there's no explicit js callback
+ if (this.op && !this.pendingJsCb) {
+ // if a CSS transition leaves immediately after enter,
+ // the transitionend event never fires. therefore we
+ // detect such cases and end the leave immediately.
+ if (this.justEntered) {
+ this.leaveDone();
+ } else {
+ pushJob(this.leaveNextTick);
+ }
+ }
+ };
+
+ /**
+ * The "nextTick" phase of a leaving transition.
+ */
+
+ p$1.leaveNextTick = function () {
+ var type = this.getCssTransitionType(this.leaveClass);
+ if (type) {
+ var event = type === TYPE_TRANSITION ? transitionEndEvent : animationEndEvent;
+ this.setupCssCb(event, this.leaveDone);
+ } else {
+ this.leaveDone();
+ }
+ };
+
+ /**
+ * The "cleanup" phase of a leaving transition.
+ */
+
+ p$1.leaveDone = function () {
+ this.left = true;
+ this.cancel = this.pendingJsCb = null;
+ this.op();
+ removeClass(this.el, this.leaveClass);
+ this.callHook('afterLeave');
+ if (this.cb) this.cb();
+ this.op = null;
+ };
+
+ /**
+ * Cancel any pending callbacks from a previously running
+ * but not finished transition.
+ */
+
+ p$1.cancelPending = function () {
+ this.op = this.cb = null;
+ var hasPending = false;
+ if (this.pendingCssCb) {
+ hasPending = true;
+ off(this.el, this.pendingCssEvent, this.pendingCssCb);
+ this.pendingCssEvent = this.pendingCssCb = null;
+ }
+ if (this.pendingJsCb) {
+ hasPending = true;
+ this.pendingJsCb.cancel();
+ this.pendingJsCb = null;
+ }
+ if (hasPending) {
+ removeClass(this.el, this.enterClass);
+ removeClass(this.el, this.leaveClass);
+ }
+ if (this.cancel) {
+ this.cancel.call(this.vm, this.el);
+ this.cancel = null;
+ }
+ };
+
+ /**
+ * Call a user-provided synchronous hook function.
+ *
+ * @param {String} type
+ */
+
+ p$1.callHook = function (type) {
+ if (this.hooks && this.hooks[type]) {
+ this.hooks[type].call(this.vm, this.el);
+ }
+ };
+
+ /**
+ * Call a user-provided, potentially-async hook function.
+ * We check for the length of arguments to see if the hook
+ * expects a `done` callback. If true, the transition's end
+ * will be determined by when the user calls that callback;
+ * otherwise, the end is determined by the CSS transition or
+ * animation.
+ *
+ * @param {String} type
+ */
+
+ p$1.callHookWithCb = function (type) {
+ var hook = this.hooks && this.hooks[type];
+ if (hook) {
+ if (hook.length > 1) {
+ this.pendingJsCb = cancellable(this[type + 'Done']);
+ }
+ hook.call(this.vm, this.el, this.pendingJsCb);
+ }
+ };
+
+ /**
+ * Get an element's transition type based on the
+ * calculated styles.
+ *
+ * @param {String} className
+ * @return {Number}
+ */
+
+ p$1.getCssTransitionType = function (className) {
+ /* istanbul ignore if */
+ if (!transitionEndEvent ||
+ // skip CSS transitions if page is not visible -
+ // this solves the issue of transitionend events not
+ // firing until the page is visible again.
+ // pageVisibility API is supported in IE10+, same as
+ // CSS transitions.
+ document.hidden ||
+ // explicit js-only transition
+ this.hooks && this.hooks.css === false ||
+ // element is hidden
+ isHidden(this.el)) {
+ return;
+ }
+ var type = this.type || this.typeCache[className];
+ if (type) return type;
+ var inlineStyles = this.el.style;
+ var computedStyles = window.getComputedStyle(this.el);
+ var transDuration = inlineStyles[transDurationProp] || computedStyles[transDurationProp];
+ if (transDuration && transDuration !== '0s') {
+ type = TYPE_TRANSITION;
+ } else {
+ var animDuration = inlineStyles[animDurationProp] || computedStyles[animDurationProp];
+ if (animDuration && animDuration !== '0s') {
+ type = TYPE_ANIMATION;
+ }
+ }
+ if (type) {
+ this.typeCache[className] = type;
+ }
+ return type;
+ };
+
+ /**
+ * Setup a CSS transitionend/animationend callback.
+ *
+ * @param {String} event
+ * @param {Function} cb
+ */
+
+ p$1.setupCssCb = function (event, cb) {
+ this.pendingCssEvent = event;
+ var self = this;
+ var el = this.el;
+ var onEnd = this.pendingCssCb = function (e) {
+ if (e.target === el) {
+ off(el, event, onEnd);
+ self.pendingCssEvent = self.pendingCssCb = null;
+ if (!self.pendingJsCb && cb) {
+ cb();
+ }
+ }
+ };
+ on(el, event, onEnd);
+ };
+
+ /**
+ * Check if an element is hidden - in that case we can just
+ * skip the transition alltogether.
+ *
+ * @param {Element} el
+ * @return {Boolean}
+ */
+
+ function isHidden(el) {
+ if (/svg$/.test(el.namespaceURI)) {
+ // SVG elements do not have offset(Width|Height)
+ // so we need to check the client rect
+ var rect = el.getBoundingClientRect();
+ return !(rect.width || rect.height);
+ } else {
+ return !(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
+ }
+ }
+
+ var transition$1 = {
+
+ priority: TRANSITION,
+
+ update: function update(id, oldId) {
+ var el = this.el;
+ // resolve on owner vm
+ var hooks = resolveAsset(this.vm.$options, 'transitions', id);
+ id = id || 'v';
+ oldId = oldId || 'v';
+ el.__v_trans = new Transition(el, id, hooks, this.vm);
+ removeClass(el, oldId + '-transition');
+ addClass(el, id + '-transition');
+ }
+ };
+
+ var internalDirectives = {
+ style: style,
+ 'class': vClass,
+ component: component,
+ prop: propDef,
+ transition: transition$1
+ };
+
+ // special binding prefixes
+ var bindRE = /^v-bind:|^:/;
+ var onRE = /^v-on:|^@/;
+ var dirAttrRE = /^v-([^:]+)(?:$|:(.*)$)/;
+ var modifierRE = /\.[^\.]+/g;
+ var transitionRE = /^(v-bind:|:)?transition$/;
+
+ // default directive priority
+ var DEFAULT_PRIORITY = 1000;
+ var DEFAULT_TERMINAL_PRIORITY = 2000;
+
+ /**
+ * Compile a template and return a reusable composite link
+ * function, which recursively contains more link functions
+ * inside. This top level compile function would normally
+ * be called on instance root nodes, but can also be used
+ * for partial compilation if the partial argument is true.
+ *
+ * The returned composite link function, when called, will
+ * return an unlink function that tearsdown all directives
+ * created during the linking phase.
+ *
+ * @param {Element|DocumentFragment} el
+ * @param {Object} options
+ * @param {Boolean} partial
+ * @return {Function}
+ */
+
+ function compile(el, options, partial) {
+ // link function for the node itself.
+ var nodeLinkFn = partial || !options._asComponent ? compileNode(el, options) : null;
+ // link function for the childNodes
+ var childLinkFn = !(nodeLinkFn && nodeLinkFn.terminal) && !isScript(el) && el.hasChildNodes() ? compileNodeList(el.childNodes, options) : null;
+
+ /**
+ * A composite linker function to be called on a already
+ * compiled piece of DOM, which instantiates all directive
+ * instances.
+ *
+ * @param {Vue} vm
+ * @param {Element|DocumentFragment} el
+ * @param {Vue} [host] - host vm of transcluded content
+ * @param {Object} [scope] - v-for scope
+ * @param {Fragment} [frag] - link context fragment
+ * @return {Function|undefined}
+ */
+
+ return function compositeLinkFn(vm, el, host, scope, frag) {
+ // cache childNodes before linking parent, fix #657
+ var childNodes = toArray(el.childNodes);
+ // link
+ var dirs = linkAndCapture(function compositeLinkCapturer() {
+ if (nodeLinkFn) nodeLinkFn(vm, el, host, scope, frag);
+ if (childLinkFn) childLinkFn(vm, childNodes, host, scope, frag);
+ }, vm);
+ return makeUnlinkFn(vm, dirs);
+ };
+ }
+
+ /**
+ * Apply a linker to a vm/element pair and capture the
+ * directives created during the process.
+ *
+ * @param {Function} linker
+ * @param {Vue} vm
+ */
+
+ function linkAndCapture(linker, vm) {
+ /* istanbul ignore if */
+ if ('development' === 'production') {}
+ var originalDirCount = vm._directives.length;
+ linker();
+ var dirs = vm._directives.slice(originalDirCount);
+ dirs.sort(directiveComparator);
+ for (var i = 0, l = dirs.length; i < l; i++) {
+ dirs[i]._bind();
+ }
+ return dirs;
+ }
+
+ /**
+ * Directive priority sort comparator
+ *
+ * @param {Object} a
+ * @param {Object} b
+ */
+
+ function directiveComparator(a, b) {
+ a = a.descriptor.def.priority || DEFAULT_PRIORITY;
+ b = b.descriptor.def.priority || DEFAULT_PRIORITY;
+ return a > b ? -1 : a === b ? 0 : 1;
+ }
+
+ /**
+ * Linker functions return an unlink function that
+ * tearsdown all directives instances generated during
+ * the process.
+ *
+ * We create unlink functions with only the necessary
+ * information to avoid retaining additional closures.
+ *
+ * @param {Vue} vm
+ * @param {Array} dirs
+ * @param {Vue} [context]
+ * @param {Array} [contextDirs]
+ * @return {Function}
+ */
+
+ function makeUnlinkFn(vm, dirs, context, contextDirs) {
+ function unlink(destroying) {
+ teardownDirs(vm, dirs, destroying);
+ if (context && contextDirs) {
+ teardownDirs(context, contextDirs);
+ }
+ }
+ // expose linked directives
+ unlink.dirs = dirs;
+ return unlink;
+ }
+
+ /**
+ * Teardown partial linked directives.
+ *
+ * @param {Vue} vm
+ * @param {Array} dirs
+ * @param {Boolean} destroying
+ */
+
+ function teardownDirs(vm, dirs, destroying) {
+ var i = dirs.length;
+ while (i--) {
+ dirs[i]._teardown();
+ if ('development' !== 'production' && !destroying) {
+ vm._directives.$remove(dirs[i]);
+ }
+ }
+ }
+
+ /**
+ * Compile link props on an instance.
+ *
+ * @param {Vue} vm
+ * @param {Element} el
+ * @param {Object} props
+ * @param {Object} [scope]
+ * @return {Function}
+ */
+
+ function compileAndLinkProps(vm, el, props, scope) {
+ var propsLinkFn = compileProps(el, props, vm);
+ var propDirs = linkAndCapture(function () {
+ propsLinkFn(vm, scope);
+ }, vm);
+ return makeUnlinkFn(vm, propDirs);
+ }
+
+ /**
+ * Compile the root element of an instance.
+ *
+ * 1. attrs on context container (context scope)
+ * 2. attrs on the component template root node, if
+ * replace:true (child scope)
+ *
+ * If this is a fragment instance, we only need to compile 1.
+ *
+ * @param {Element} el
+ * @param {Object} options
+ * @param {Object} contextOptions
+ * @return {Function}
+ */
+
+ function compileRoot(el, options, contextOptions) {
+ var containerAttrs = options._containerAttrs;
+ var replacerAttrs = options._replacerAttrs;
+ var contextLinkFn, replacerLinkFn;
+
+ // only need to compile other attributes for
+ // non-fragment instances
+ if (el.nodeType !== 11) {
+ // for components, container and replacer need to be
+ // compiled separately and linked in different scopes.
+ if (options._asComponent) {
+ // 2. container attributes
+ if (containerAttrs && contextOptions) {
+ contextLinkFn = compileDirectives(containerAttrs, contextOptions);
+ }
+ if (replacerAttrs) {
+ // 3. replacer attributes
+ replacerLinkFn = compileDirectives(replacerAttrs, options);
+ }
+ } else {
+ // non-component, just compile as a normal element.
+ replacerLinkFn = compileDirectives(el.attributes, options);
+ }
+ } else if ('development' !== 'production' && containerAttrs) {
+ // warn container directives for fragment instances
+ var names = containerAttrs.filter(function (attr) {
+ // allow vue-loader/vueify scoped css attributes
+ return attr.name.indexOf('_v-') < 0 &&
+ // allow event listeners
+ !onRE.test(attr.name) &&
+ // allow slots
+ attr.name !== 'slot';
+ }).map(function (attr) {
+ return '"' + attr.name + '"';
+ });
+ if (names.length) {
+ var plural = names.length > 1;
+ warn('Attribute' + (plural ? 's ' : ' ') + names.join(', ') + (plural ? ' are' : ' is') + ' ignored on component ' + '<' + options.el.tagName.toLowerCase() + '> because ' + 'the component is a fragment instance: ' + 'http://vuejs.org/guide/components.html#Fragment-Instance');
+ }
+ }
+
+ options._containerAttrs = options._replacerAttrs = null;
+ return function rootLinkFn(vm, el, scope) {
+ // link context scope dirs
+ var context = vm._context;
+ var contextDirs;
+ if (context && contextLinkFn) {
+ contextDirs = linkAndCapture(function () {
+ contextLinkFn(context, el, null, scope);
+ }, context);
+ }
+
+ // link self
+ var selfDirs = linkAndCapture(function () {
+ if (replacerLinkFn) replacerLinkFn(vm, el);
+ }, vm);
+
+ // return the unlink function that tearsdown context
+ // container directives.
+ return makeUnlinkFn(vm, selfDirs, context, contextDirs);
+ };
+ }
+
+ /**
+ * Compile a node and return a nodeLinkFn based on the
+ * node type.
+ *
+ * @param {Node} node
+ * @param {Object} options
+ * @return {Function|null}
+ */
+
+ function compileNode(node, options) {
+ var type = node.nodeType;
+ if (type === 1 && !isScript(node)) {
+ return compileElement(node, options);
+ } else if (type === 3 && node.data.trim()) {
+ return compileTextNode(node, options);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Compile an element and return a nodeLinkFn.
+ *
+ * @param {Element} el
+ * @param {Object} options
+ * @return {Function|null}
+ */
+
+ function compileElement(el, options) {
+ // preprocess textareas.
+ // textarea treats its text content as the initial value.
+ // just bind it as an attr directive for value.
+ if (el.tagName === 'TEXTAREA') {
+ var tokens = parseText(el.value);
+ if (tokens) {
+ el.setAttribute(':value', tokensToExp(tokens));
+ el.value = '';
+ }
+ }
+ var linkFn;
+ var hasAttrs = el.hasAttributes();
+ var attrs = hasAttrs && toArray(el.attributes);
+ // check terminal directives (for & if)
+ if (hasAttrs) {
+ linkFn = checkTerminalDirectives(el, attrs, options);
+ }
+ // check element directives
+ if (!linkFn) {
+ linkFn = checkElementDirectives(el, options);
+ }
+ // check component
+ if (!linkFn) {
+ linkFn = checkComponent(el, options);
+ }
+ // normal directives
+ if (!linkFn && hasAttrs) {
+ linkFn = compileDirectives(attrs, options);
+ }
+ return linkFn;
+ }
+
+ /**
+ * Compile a textNode and return a nodeLinkFn.
+ *
+ * @param {TextNode} node
+ * @param {Object} options
+ * @return {Function|null} textNodeLinkFn
+ */
+
+ function compileTextNode(node, options) {
+ // skip marked text nodes
+ if (node._skip) {
+ return removeText;
+ }
+
+ var tokens = parseText(node.wholeText);
+ if (!tokens) {
+ return null;
+ }
+
+ // mark adjacent text nodes as skipped,
+ // because we are using node.wholeText to compile
+ // all adjacent text nodes together. This fixes
+ // issues in IE where sometimes it splits up a single
+ // text node into multiple ones.
+ var next = node.nextSibling;
+ while (next && next.nodeType === 3) {
+ next._skip = true;
+ next = next.nextSibling;
+ }
+
+ var frag = document.createDocumentFragment();
+ var el, token;
+ for (var i = 0, l = tokens.length; i < l; i++) {
+ token = tokens[i];
+ el = token.tag ? processTextToken(token, options) : document.createTextNode(token.value);
+ frag.appendChild(el);
+ }
+ return makeTextNodeLinkFn(tokens, frag, options);
+ }
+
+ /**
+ * Linker for an skipped text node.
+ *
+ * @param {Vue} vm
+ * @param {Text} node
+ */
+
+ function removeText(vm, node) {
+ remove(node);
+ }
+
+ /**
+ * Process a single text token.
+ *
+ * @param {Object} token
+ * @param {Object} options
+ * @return {Node}
+ */
+
+ function processTextToken(token, options) {
+ var el;
+ if (token.oneTime) {
+ el = document.createTextNode(token.value);
+ } else {
+ if (token.html) {
+ el = document.createComment('v-html');
+ setTokenType('html');
+ } else {
+ // IE will clean up empty textNodes during
+ // frag.cloneNode(true), so we have to give it
+ // something here...
+ el = document.createTextNode(' ');
+ setTokenType('text');
+ }
+ }
+ function setTokenType(type) {
+ if (token.descriptor) return;
+ var parsed = parseDirective(token.value);
+ token.descriptor = {
+ name: type,
+ def: directives[type],
+ expression: parsed.expression,
+ filters: parsed.filters
+ };
+ }
+ return el;
+ }
+
+ /**
+ * Build a function that processes a textNode.
+ *
+ * @param {Array<Object>} tokens
+ * @param {DocumentFragment} frag
+ */
+
+ function makeTextNodeLinkFn(tokens, frag) {
+ return function textNodeLinkFn(vm, el, host, scope) {
+ var fragClone = frag.cloneNode(true);
+ var childNodes = toArray(fragClone.childNodes);
+ var token, value, node;
+ for (var i = 0, l = tokens.length; i < l; i++) {
+ token = tokens[i];
+ value = token.value;
+ if (token.tag) {
+ node = childNodes[i];
+ if (token.oneTime) {
+ value = (scope || vm).$eval(value);
+ if (token.html) {
+ replace(node, parseTemplate(value, true));
+ } else {
+ node.data = _toString(value);
+ }
+ } else {
+ vm._bindDir(token.descriptor, node, host, scope);
+ }
+ }
+ }
+ replace(el, fragClone);
+ };
+ }
+
+ /**
+ * Compile a node list and return a childLinkFn.
+ *
+ * @param {NodeList} nodeList
+ * @param {Object} options
+ * @return {Function|undefined}
+ */
+
+ function compileNodeList(nodeList, options) {
+ var linkFns = [];
+ var nodeLinkFn, childLinkFn, node;
+ for (var i = 0, l = nodeList.length; i < l; i++) {
+ node = nodeList[i];
+ nodeLinkFn = compileNode(node, options);
+ childLinkFn = !(nodeLinkFn && nodeLinkFn.terminal) && node.tagName !== 'SCRIPT' && node.hasChildNodes() ? compileNodeList(node.childNodes, options) : null;
+ linkFns.push(nodeLinkFn, childLinkFn);
+ }
+ return linkFns.length ? makeChildLinkFn(linkFns) : null;
+ }
+
+ /**
+ * Make a child link function for a node's childNodes.
+ *
+ * @param {Array<Function>} linkFns
+ * @return {Function} childLinkFn
+ */
+
+ function makeChildLinkFn(linkFns) {
+ return function childLinkFn(vm, nodes, host, scope, frag) {
+ var node, nodeLinkFn, childrenLinkFn;
+ for (var i = 0, n = 0, l = linkFns.length; i < l; n++) {
+ node = nodes[n];
+ nodeLinkFn = linkFns[i++];
+ childrenLinkFn = linkFns[i++];
+ // cache childNodes before linking parent, fix #657
+ var childNodes = toArray(node.childNodes);
+ if (nodeLinkFn) {
+ nodeLinkFn(vm, node, host, scope, frag);
+ }
+ if (childrenLinkFn) {
+ childrenLinkFn(vm, childNodes, host, scope, frag);
+ }
+ }
+ };
+ }
+
+ /**
+ * Check for element directives (custom elements that should
+ * be resovled as terminal directives).
+ *
+ * @param {Element} el
+ * @param {Object} options
+ */
+
+ function checkElementDirectives(el, options) {
+ var tag = el.tagName.toLowerCase();
+ if (commonTagRE.test(tag)) {
+ return;
+ }
+ var def = resolveAsset(options, 'elementDirectives', tag);
+ if (def) {
+ return makeTerminalNodeLinkFn(el, tag, '', options, def);
+ }
+ }
+
+ /**
+ * Check if an element is a component. If yes, return
+ * a component link function.
+ *
+ * @param {Element} el
+ * @param {Object} options
+ * @return {Function|undefined}
+ */
+
+ function checkComponent(el, options) {
+ var component = checkComponentAttr(el, options);
+ if (component) {
+ var ref = findRef(el);
+ var descriptor = {
+ name: 'component',
+ ref: ref,
+ expression: component.id,
+ def: internalDirectives.component,
+ modifiers: {
+ literal: !component.dynamic
+ }
+ };
+ var componentLinkFn = function componentLinkFn(vm, el, host, scope, frag) {
+ if (ref) {
+ defineReactive((scope || vm).$refs, ref, null);
+ }
+ vm._bindDir(descriptor, el, host, scope, frag);
+ };
+ componentLinkFn.terminal = true;
+ return componentLinkFn;
+ }
+ }
+
+ /**
+ * Check an element for terminal directives in fixed order.
+ * If it finds one, return a terminal link function.
+ *
+ * @param {Element} el
+ * @param {Array} attrs
+ * @param {Object} options
+ * @return {Function} terminalLinkFn
+ */
+
+ function checkTerminalDirectives(el, attrs, options) {
+ // skip v-pre
+ if (getAttr(el, 'v-pre') !== null) {
+ return skip;
+ }
+ // skip v-else block, but only if following v-if
+ if (el.hasAttribute('v-else')) {
+ var prev = el.previousElementSibling;
+ if (prev && prev.hasAttribute('v-if')) {
+ return skip;
+ }
+ }
+
+ var attr, name, value, modifiers, matched, dirName, rawName, arg, def, termDef;
+ for (var i = 0, j = attrs.length; i < j; i++) {
+ attr = attrs[i];
+ name = attr.name.replace(modifierRE, '');
+ if (matched = name.match(dirAttrRE)) {
+ def = resolveAsset(options, 'directives', matched[1]);
+ if (def && def.terminal) {
+ if (!termDef || (def.priority || DEFAULT_TERMINAL_PRIORITY) > termDef.priority) {
+ termDef = def;
+ rawName = attr.name;
+ modifiers = parseModifiers(attr.name);
+ value = attr.value;
+ dirName = matched[1];
+ arg = matched[2];
+ }
+ }
+ }
+ }
+
+ if (termDef) {
+ return makeTerminalNodeLinkFn(el, dirName, value, options, termDef, rawName, arg, modifiers);
+ }
+ }
+
+ function skip() {}
+ skip.terminal = true;
+
+ /**
+ * Build a node link function for a terminal directive.
+ * A terminal link function terminates the current
+ * compilation recursion and handles compilation of the
+ * subtree in the directive.
+ *
+ * @param {Element} el
+ * @param {String} dirName
+ * @param {String} value
+ * @param {Object} options
+ * @param {Object} def
+ * @param {String} [rawName]
+ * @param {String} [arg]
+ * @param {Object} [modifiers]
+ * @return {Function} terminalLinkFn
+ */
+
+ function makeTerminalNodeLinkFn(el, dirName, value, options, def, rawName, arg, modifiers) {
+ var parsed = parseDirective(value);
+ var descriptor = {
+ name: dirName,
+ arg: arg,
+ expression: parsed.expression,
+ filters: parsed.filters,
+ raw: value,
+ attr: rawName,
+ modifiers: modifiers,
+ def: def
+ };
+ // check ref for v-for and router-view
+ if (dirName === 'for' || dirName === 'router-view') {
+ descriptor.ref = findRef(el);
+ }
+ var fn = function terminalNodeLinkFn(vm, el, host, scope, frag) {
+ if (descriptor.ref) {
+ defineReactive((scope || vm).$refs, descriptor.ref, null);
+ }
+ vm._bindDir(descriptor, el, host, scope, frag);
+ };
+ fn.terminal = true;
+ return fn;
+ }
+
+ /**
+ * Compile the directives on an element and return a linker.
+ *
+ * @param {Array|NamedNodeMap} attrs
+ * @param {Object} options
+ * @return {Function}
+ */
+
+ function compileDirectives(attrs, options) {
+ var i = attrs.length;
+ var dirs = [];
+ var attr, name, value, rawName, rawValue, dirName, arg, modifiers, dirDef, tokens, matched;
+ while (i--) {
+ attr = attrs[i];
+ name = rawName = attr.name;
+ value = rawValue = attr.value;
+ tokens = parseText(value);
+ // reset arg
+ arg = null;
+ // check modifiers
+ modifiers = parseModifiers(name);
+ name = name.replace(modifierRE, '');
+
+ // attribute interpolations
+ if (tokens) {
+ value = tokensToExp(tokens);
+ arg = name;
+ pushDir('bind', directives.bind, tokens);
+ // warn against mixing mustaches with v-bind
+ if ('development' !== 'production') {
+ if (name === 'class' && Array.prototype.some.call(attrs, function (attr) {
+ return attr.name === ':class' || attr.name === 'v-bind:class';
+ })) {
+ warn('class="' + rawValue + '": Do not mix mustache interpolation ' + 'and v-bind for "class" on the same element. Use one or the other.', options);
+ }
+ }
+ } else
+
+ // special attribute: transition
+ if (transitionRE.test(name)) {
+ modifiers.literal = !bindRE.test(name);
+ pushDir('transition', internalDirectives.transition);
+ } else
+
+ // event handlers
+ if (onRE.test(name)) {
+ arg = name.replace(onRE, '');
+ pushDir('on', directives.on);
+ } else
+
+ // attribute bindings
+ if (bindRE.test(name)) {
+ dirName = name.replace(bindRE, '');
+ if (dirName === 'style' || dirName === 'class') {
+ pushDir(dirName, internalDirectives[dirName]);
+ } else {
+ arg = dirName;
+ pushDir('bind', directives.bind);
+ }
+ } else
+
+ // normal directives
+ if (matched = name.match(dirAttrRE)) {
+ dirName = matched[1];
+ arg = matched[2];
+
+ // skip v-else (when used with v-show)
+ if (dirName === 'else') {
+ continue;
+ }
+
+ dirDef = resolveAsset(options, 'directives', dirName, true);
+ if (dirDef) {
+ pushDir(dirName, dirDef);
+ }
+ }
+ }
+
+ /**
+ * Push a directive.
+ *
+ * @param {String} dirName
+ * @param {Object|Function} def
+ * @param {Array} [interpTokens]
+ */
+
+ function pushDir(dirName, def, interpTokens) {
+ var hasOneTimeToken = interpTokens && hasOneTime(interpTokens);
+ var parsed = !hasOneTimeToken && parseDirective(value);
+ dirs.push({
+ name: dirName,
+ attr: rawName,
+ raw: rawValue,
+ def: def,
+ arg: arg,
+ modifiers: modifiers,
+ // conversion from interpolation strings with one-time token
+ // to expression is differed until directive bind time so that we
+ // have access to the actual vm context for one-time bindings.
+ expression: parsed && parsed.expression,
+ filters: parsed && parsed.filters,
+ interp: interpTokens,
+ hasOneTime: hasOneTimeToken
+ });
+ }
+
+ if (dirs.length) {
+ return makeNodeLinkFn(dirs);
+ }
+ }
+
+ /**
+ * Parse modifiers from directive attribute name.
+ *
+ * @param {String} name
+ * @return {Object}
+ */
+
+ function parseModifiers(name) {
+ var res = Object.create(null);
+ var match = name.match(modifierRE);
+ if (match) {
+ var i = match.length;
+ while (i--) {
+ res[match[i].slice(1)] = true;
+ }
+ }
+ return res;
+ }
+
+ /**
+ * Build a link function for all directives on a single node.
+ *
+ * @param {Array} directives
+ * @return {Function} directivesLinkFn
+ */
+
+ function makeNodeLinkFn(directives) {
+ return function nodeLinkFn(vm, el, host, scope, frag) {
+ // reverse apply because it's sorted low to high
+ var i = directives.length;
+ while (i--) {
+ vm._bindDir(directives[i], el, host, scope, frag);
+ }
+ };
+ }
+
+ /**
+ * Check if an interpolation string contains one-time tokens.
+ *
+ * @param {Array} tokens
+ * @return {Boolean}
+ */
+
+ function hasOneTime(tokens) {
+ var i = tokens.length;
+ while (i--) {
+ if (tokens[i].oneTime) return true;
+ }
+ }
+
+ function isScript(el) {
+ return el.tagName === 'SCRIPT' && (!el.hasAttribute('type') || el.getAttribute('type') === 'text/javascript');
+ }
+
+ var specialCharRE = /[^\w\-:\.]/;
+
+ /**
+ * Process an element or a DocumentFragment based on a
+ * instance option object. This allows us to transclude
+ * a template node/fragment before the instance is created,
+ * so the processed fragment can then be cloned and reused
+ * in v-for.
+ *
+ * @param {Element} el
+ * @param {Object} options
+ * @return {Element|DocumentFragment}
+ */
+
+ function transclude(el, options) {
+ // extract container attributes to pass them down
+ // to compiler, because they need to be compiled in
+ // parent scope. we are mutating the options object here
+ // assuming the same object will be used for compile
+ // right after this.
+ if (options) {
+ options._containerAttrs = extractAttrs(el);
+ }
+ // for template tags, what we want is its content as
+ // a documentFragment (for fragment instances)
+ if (isTemplate(el)) {
+ el = parseTemplate(el);
+ }
+ if (options) {
+ if (options._asComponent && !options.template) {
+ options.template = '<slot></slot>';
+ }
+ if (options.template) {
+ options._content = extractContent(el);
+ el = transcludeTemplate(el, options);
+ }
+ }
+ if (isFragment(el)) {
+ // anchors for fragment instance
+ // passing in `persist: true` to avoid them being
+ // discarded by IE during template cloning
+ prepend(createAnchor('v-start', true), el);
+ el.appendChild(createAnchor('v-end', true));
+ }
+ return el;
+ }
+
+ /**
+ * Process the template option.
+ * If the replace option is true this will swap the $el.
+ *
+ * @param {Element} el
+ * @param {Object} options
+ * @return {Element|DocumentFragment}
+ */
+
+ function transcludeTemplate(el, options) {
+ var template = options.template;
+ var frag = parseTemplate(template, true);
+ if (frag) {
+ var replacer = frag.firstChild;
+ var tag = replacer.tagName && replacer.tagName.toLowerCase();
+ if (options.replace) {
+ /* istanbul ignore if */
+ if (el === document.body) {
+ 'development' !== 'production' && warn('You are mounting an instance with a template to ' + '<body>. This will replace <body> entirely. You ' + 'should probably use `replace: false` here.');
+ }
+ // there are many cases where the instance must
+ // become a fragment instance: basically anything that
+ // can create more than 1 root nodes.
+ if (
+ // multi-children template
+ frag.childNodes.length > 1 ||
+ // non-element template
+ replacer.nodeType !== 1 ||
+ // single nested component
+ tag === 'component' || resolveAsset(options, 'components', tag) || hasBindAttr(replacer, 'is') ||
+ // element directive
+ resolveAsset(options, 'elementDirectives', tag) ||
+ // for block
+ replacer.hasAttribute('v-for') ||
+ // if block
+ replacer.hasAttribute('v-if')) {
+ return frag;
+ } else {
+ options._replacerAttrs = extractAttrs(replacer);
+ mergeAttrs(el, replacer);
+ return replacer;
+ }
+ } else {
+ el.appendChild(frag);
+ return el;
+ }
+ } else {
+ 'development' !== 'production' && warn('Invalid template option: ' + template);
+ }
+ }
+
+ /**
+ * Helper to extract a component container's attributes
+ * into a plain object array.
+ *
+ * @param {Element} el
+ * @return {Array}
+ */
+
+ function extractAttrs(el) {
+ if (el.nodeType === 1 && el.hasAttributes()) {
+ return toArray(el.attributes);
+ }
+ }
+
+ /**
+ * Merge the attributes of two elements, and make sure
+ * the class names are merged properly.
+ *
+ * @param {Element} from
+ * @param {Element} to
+ */
+
+ function mergeAttrs(from, to) {
+ var attrs = from.attributes;
+ var i = attrs.length;
+ var name, value;
+ while (i--) {
+ name = attrs[i].name;
+ value = attrs[i].value;
+ if (!to.hasAttribute(name) && !specialCharRE.test(name)) {
+ to.setAttribute(name, value);
+ } else if (name === 'class' && !parseText(value) && (value = value.trim())) {
+ value.split(/\s+/).forEach(function (cls) {
+ addClass(to, cls);
+ });
+ }
+ }
+ }
+
+ /**
+ * Scan and determine slot content distribution.
+ * We do this during transclusion instead at compile time so that
+ * the distribution is decoupled from the compilation order of
+ * the slots.
+ *
+ * @param {Element|DocumentFragment} template
+ * @param {Element} content
+ * @param {Vue} vm
+ */
+
+ function resolveSlots(vm, content) {
+ if (!content) {
+ return;
+ }
+ var contents = vm._slotContents = Object.create(null);
+ var el, name;
+ for (var i = 0, l = content.children.length; i < l; i++) {
+ el = content.children[i];
+ /* eslint-disable no-cond-assign */
+ if (name = el.getAttribute('slot')) {
+ (contents[name] || (contents[name] = [])).push(el);
+ }
+ /* eslint-enable no-cond-assign */
+ if ('development' !== 'production' && getBindAttr(el, 'slot')) {
+ warn('The "slot" attribute must be static.', vm.$parent);
+ }
+ }
+ for (name in contents) {
+ contents[name] = extractFragment(contents[name], content);
+ }
+ if (content.hasChildNodes()) {
+ var nodes = content.childNodes;
+ if (nodes.length === 1 && nodes[0].nodeType === 3 && !nodes[0].data.trim()) {
+ return;
+ }
+ contents['default'] = extractFragment(content.childNodes, content);
+ }
+ }
+
+ /**
+ * Extract qualified content nodes from a node list.
+ *
+ * @param {NodeList} nodes
+ * @return {DocumentFragment}
+ */
+
+ function extractFragment(nodes, parent) {
+ var frag = document.createDocumentFragment();
+ nodes = toArray(nodes);
+ for (var i = 0, l = nodes.length; i < l; i++) {
+ var node = nodes[i];
+ if (isTemplate(node) && !node.hasAttribute('v-if') && !node.hasAttribute('v-for')) {
+ parent.removeChild(node);
+ node = parseTemplate(node, true);
+ }
+ frag.appendChild(node);
+ }
+ return frag;
+ }
+
+
+
+ var compiler = Object.freeze({
+ compile: compile,
+ compileAndLinkProps: compileAndLinkProps,
+ compileRoot: compileRoot,
+ transclude: transclude,
+ resolveSlots: resolveSlots
+ });
+
+ function stateMixin (Vue) {
+ /**
+ * Accessor for `$data` property, since setting $data
+ * requires observing the new object and updating
+ * proxied properties.
+ */
+
+ Object.defineProperty(Vue.prototype, '$data', {
+ get: function get() {
+ return this._data;
+ },
+ set: function set(newData) {
+ if (newData !== this._data) {
+ this._setData(newData);
+ }
+ }
+ });
+
+ /**
+ * Setup the scope of an instance, which contains:
+ * - observed data
+ * - computed properties
+ * - user methods
+ * - meta properties
+ */
+
+ Vue.prototype._initState = function () {
+ this._initProps();
+ this._initMeta();
+ this._initMethods();
+ this._initData();
+ this._initComputed();
+ };
+
+ /**
+ * Initialize props.
+ */
+
+ Vue.prototype._initProps = function () {
+ var options = this.$options;
+ var el = options.el;
+ var props = options.props;
+ if (props && !el) {
+ 'development' !== 'production' && warn('Props will not be compiled if no `el` option is ' + 'provided at instantiation.', this);
+ }
+ // make sure to convert string selectors into element now
+ el = options.el = query(el);
+ this._propsUnlinkFn = el && el.nodeType === 1 && props
+ // props must be linked in proper scope if inside v-for
+ ? compileAndLinkProps(this, el, props, this._scope) : null;
+ };
+
+ /**
+ * Initialize the data.
+ */
+
+ Vue.prototype._initData = function () {
+ var dataFn = this.$options.data;
+ var data = this._data = dataFn ? dataFn() : {};
+ if (!isPlainObject(data)) {
+ data = {};
+ 'development' !== 'production' && warn('data functions should return an object.', this);
+ }
+ var props = this._props;
+ // proxy data on instance
+ var keys = Object.keys(data);
+ var i, key;
+ i = keys.length;
+ while (i--) {
+ key = keys[i];
+ // there are two scenarios where we can proxy a data key:
+ // 1. it's not already defined as a prop
+ // 2. it's provided via a instantiation option AND there are no
+ // template prop present
+ if (!props || !hasOwn(props, key)) {
+ this._proxy(key);
+ } else if ('development' !== 'production') {
+ warn('Data field "' + key + '" is already defined ' + 'as a prop. To provide default value for a prop, use the "default" ' + 'prop option; if you want to pass prop values to an instantiation ' + 'call, use the "propsData" option.', this);
+ }
+ }
+ // observe data
+ observe(data, this);
+ };
+
+ /**
+ * Swap the instance's $data. Called in $data's setter.
+ *
+ * @param {Object} newData
+ */
+
+ Vue.prototype._setData = function (newData) {
+ newData = newData || {};
+ var oldData = this._data;
+ this._data = newData;
+ var keys, key, i;
+ // unproxy keys not present in new data
+ keys = Object.keys(oldData);
+ i = keys.length;
+ while (i--) {
+ key = keys[i];
+ if (!(key in newData)) {
+ this._unproxy(key);
+ }
+ }
+ // proxy keys not already proxied,
+ // and trigger change for changed values
+ keys = Object.keys(newData);
+ i = keys.length;
+ while (i--) {
+ key = keys[i];
+ if (!hasOwn(this, key)) {
+ // new property
+ this._proxy(key);
+ }
+ }
+ oldData.__ob__.removeVm(this);
+ observe(newData, this);
+ this._digest();
+ };
+
+ /**
+ * Proxy a property, so that
+ * vm.prop === vm._data.prop
+ *
+ * @param {String} key
+ */
+
+ Vue.prototype._proxy = function (key) {
+ if (!isReserved(key)) {
+ // need to store ref to self here
+ // because these getter/setters might
+ // be called by child scopes via
+ // prototype inheritance.
+ var self = this;
+ Object.defineProperty(self, key, {
+ configurable: true,
+ enumerable: true,
+ get: function proxyGetter() {
+ return self._data[key];
+ },
+ set: function proxySetter(val) {
+ self._data[key] = val;
+ }
+ });
+ }
+ };
+
+ /**
+ * Unproxy a property.
+ *
+ * @param {String} key
+ */
+
+ Vue.prototype._unproxy = function (key) {
+ if (!isReserved(key)) {
+ delete this[key];
+ }
+ };
+
+ /**
+ * Force update on every watcher in scope.
+ */
+
+ Vue.prototype._digest = function () {
+ for (var i = 0, l = this._watchers.length; i < l; i++) {
+ this._watchers[i].update(true); // shallow updates
+ }
+ };
+
+ /**
+ * Setup computed properties. They are essentially
+ * special getter/setters
+ */
+
+ function noop() {}
+ Vue.prototype._initComputed = function () {
+ var computed = this.$options.computed;
+ if (computed) {
+ for (var key in computed) {
+ var userDef = computed[key];
+ var def = {
+ enumerable: true,
+ configurable: true
+ };
+ if (typeof userDef === 'function') {
+ def.get = makeComputedGetter(userDef, this);
+ def.set = noop;
+ } else {
+ def.get = userDef.get ? userDef.cache !== false ? makeComputedGetter(userDef.get, this) : bind(userDef.get, this) : noop;
+ def.set = userDef.set ? bind(userDef.set, this) : noop;
+ }
+ Object.defineProperty(this, key, def);
+ }
+ }
+ };
+
+ function makeComputedGetter(getter, owner) {
+ var watcher = new Watcher(owner, getter, null, {
+ lazy: true
+ });
+ return function computedGetter() {
+ if (watcher.dirty) {
+ watcher.evaluate();
+ }
+ if (Dep.target) {
+ watcher.depend();
+ }
+ return watcher.value;
+ };
+ }
+
+ /**
+ * Setup instance methods. Methods must be bound to the
+ * instance since they might be passed down as a prop to
+ * child components.
+ */
+
+ Vue.prototype._initMethods = function () {
+ var methods = this.$options.methods;
+ if (methods) {
+ for (var key in methods) {
+ this[key] = bind(methods[key], this);
+ }
+ }
+ };
+
+ /**
+ * Initialize meta information like $index, $key & $value.
+ */
+
+ Vue.prototype._initMeta = function () {
+ var metas = this.$options._meta;
+ if (metas) {
+ for (var key in metas) {
+ defineReactive(this, key, metas[key]);
+ }
+ }
+ };
+ }
+
+ var eventRE = /^v-on:|^@/;
+
+ function eventsMixin (Vue) {
+ /**
+ * Setup the instance's option events & watchers.
+ * If the value is a string, we pull it from the
+ * instance's methods by name.
+ */
+
+ Vue.prototype._initEvents = function () {
+ var options = this.$options;
+ if (options._asComponent) {
+ registerComponentEvents(this, options.el);
+ }
+ registerCallbacks(this, '$on', options.events);
+ registerCallbacks(this, '$watch', options.watch);
+ };
+
+ /**
+ * Register v-on events on a child component
+ *
+ * @param {Vue} vm
+ * @param {Element} el
+ */
+
+ function registerComponentEvents(vm, el) {
+ var attrs = el.attributes;
+ var name, value, handler;
+ for (var i = 0, l = attrs.length; i < l; i++) {
+ name = attrs[i].name;
+ if (eventRE.test(name)) {
+ name = name.replace(eventRE, '');
+ // force the expression into a statement so that
+ // it always dynamically resolves the method to call (#2670)
+ // kinda ugly hack, but does the job.
+ value = attrs[i].value;
+ if (isSimplePath(value)) {
+ value += '.apply(this, $arguments)';
+ }
+ handler = (vm._scope || vm._context).$eval(value, true);
+ handler._fromParent = true;
+ vm.$on(name.replace(eventRE), handler);
+ }
+ }
+ }
+
+ /**
+ * Register callbacks for option events and watchers.
+ *
+ * @param {Vue} vm
+ * @param {String} action
+ * @param {Object} hash
+ */
+
+ function registerCallbacks(vm, action, hash) {
+ if (!hash) return;
+ var handlers, key, i, j;
+ for (key in hash) {
+ handlers = hash[key];
+ if (isArray(handlers)) {
+ for (i = 0, j = handlers.length; i < j; i++) {
+ register(vm, action, key, handlers[i]);
+ }
+ } else {
+ register(vm, action, key, handlers);
+ }
+ }
+ }
+
+ /**
+ * Helper to register an event/watch callback.
+ *
+ * @param {Vue} vm
+ * @param {String} action
+ * @param {String} key
+ * @param {Function|String|Object} handler
+ * @param {Object} [options]
+ */
+
+ function register(vm, action, key, handler, options) {
+ var type = typeof handler;
+ if (type === 'function') {
+ vm[action](key, handler, options);
+ } else if (type === 'string') {
+ var methods = vm.$options.methods;
+ var method = methods && methods[handler];
+ if (method) {
+ vm[action](key, method, options);
+ } else {
+ 'development' !== 'production' && warn('Unknown method: "' + handler + '" when ' + 'registering callback for ' + action + ': "' + key + '".', vm);
+ }
+ } else if (handler && type === 'object') {
+ register(vm, action, key, handler.handler, handler);
+ }
+ }
+
+ /**
+ * Setup recursive attached/detached calls
+ */
+
+ Vue.prototype._initDOMHooks = function () {
+ this.$on('hook:attached', onAttached);
+ this.$on('hook:detached', onDetached);
+ };
+
+ /**
+ * Callback to recursively call attached hook on children
+ */
+
+ function onAttached() {
+ if (!this._isAttached) {
+ this._isAttached = true;
+ this.$children.forEach(callAttach);
+ }
+ }
+
+ /**
+ * Iterator to call attached hook
+ *
+ * @param {Vue} child
+ */
+
+ function callAttach(child) {
+ if (!child._isAttached && inDoc(child.$el)) {
+ child._callHook('attached');
+ }
+ }
+
+ /**
+ * Callback to recursively call detached hook on children
+ */
+
+ function onDetached() {
+ if (this._isAttached) {
+ this._isAttached = false;
+ this.$children.forEach(callDetach);
+ }
+ }
+
+ /**
+ * Iterator to call detached hook
+ *
+ * @param {Vue} child
+ */
+
+ function callDetach(child) {
+ if (child._isAttached && !inDoc(child.$el)) {
+ child._callHook('detached');
+ }
+ }
+
+ /**
+ * Trigger all handlers for a hook
+ *
+ * @param {String} hook
+ */
+
+ Vue.prototype._callHook = function (hook) {
+ this.$emit('pre-hook:' + hook);
+ var handlers = this.$options[hook];
+ if (handlers) {
+ for (var i = 0, j = handlers.length; i < j; i++) {
+ handlers[i].call(this);
+ }
+ }
+ this.$emit('hook:' + hook);
+ };
+ }
+
+ function noop$1() {}
+
+ /**
+ * A directive links a DOM element with a piece of data,
+ * which is the result of evaluating an expression.
+ * It registers a watcher with the expression and calls
+ * the DOM update function when a change is triggered.
+ *
+ * @param {Object} descriptor
+ * - {String} name
+ * - {Object} def
+ * - {String} expression
+ * - {Array<Object>} [filters]
+ * - {Object} [modifiers]
+ * - {Boolean} literal
+ * - {String} attr
+ * - {String} arg
+ * - {String} raw
+ * - {String} [ref]
+ * - {Array<Object>} [interp]
+ * - {Boolean} [hasOneTime]
+ * @param {Vue} vm
+ * @param {Node} el
+ * @param {Vue} [host] - transclusion host component
+ * @param {Object} [scope] - v-for scope
+ * @param {Fragment} [frag] - owner fragment
+ * @constructor
+ */
+ function Directive(descriptor, vm, el, host, scope, frag) {
+ this.vm = vm;
+ this.el = el;
+ // copy descriptor properties
+ this.descriptor = descriptor;
+ this.name = descriptor.name;
+ this.expression = descriptor.expression;
+ this.arg = descriptor.arg;
+ this.modifiers = descriptor.modifiers;
+ this.filters = descriptor.filters;
+ this.literal = this.modifiers && this.modifiers.literal;
+ // private
+ this._locked = false;
+ this._bound = false;
+ this._listeners = null;
+ // link context
+ this._host = host;
+ this._scope = scope;
+ this._frag = frag;
+ // store directives on node in dev mode
+ if ('development' !== 'production' && this.el) {
+ this.el._vue_directives = this.el._vue_directives || [];
+ this.el._vue_directives.push(this);
+ }
+ }
+
+ /**
+ * Initialize the directive, mixin definition properties,
+ * setup the watcher, call definition bind() and update()
+ * if present.
+ */
+
+ Directive.prototype._bind = function () {
+ var name = this.name;
+ var descriptor = this.descriptor;
+
+ // remove attribute
+ if ((name !== 'cloak' || this.vm._isCompiled) && this.el && this.el.removeAttribute) {
+ var attr = descriptor.attr || 'v-' + name;
+ this.el.removeAttribute(attr);
+ }
+
+ // copy def properties
+ var def = descriptor.def;
+ if (typeof def === 'function') {
+ this.update = def;
+ } else {
+ extend(this, def);
+ }
+
+ // setup directive params
+ this._setupParams();
+
+ // initial bind
+ if (this.bind) {
+ this.bind();
+ }
+ this._bound = true;
+
+ if (this.literal) {
+ this.update && this.update(descriptor.raw);
+ } else if ((this.expression || this.modifiers) && (this.update || this.twoWay) && !this._checkStatement()) {
+ // wrapped updater for context
+ var dir = this;
+ if (this.update) {
+ this._update = function (val, oldVal) {
+ if (!dir._locked) {
+ dir.update(val, oldVal);
+ }
+ };
+ } else {
+ this._update = noop$1;
+ }
+ var preProcess = this._preProcess ? bind(this._preProcess, this) : null;
+ var postProcess = this._postProcess ? bind(this._postProcess, this) : null;
+ var watcher = this._watcher = new Watcher(this.vm, this.expression, this._update, // callback
+ {
+ filters: this.filters,
+ twoWay: this.twoWay,
+ deep: this.deep,
+ preProcess: preProcess,
+ postProcess: postProcess,
+ scope: this._scope
+ });
+ // v-model with inital inline value need to sync back to
+ // model instead of update to DOM on init. They would
+ // set the afterBind hook to indicate that.
+ if (this.afterBind) {
+ this.afterBind();
+ } else if (this.update) {
+ this.update(watcher.value);
+ }
+ }
+ };
+
+ /**
+ * Setup all param attributes, e.g. track-by,
+ * transition-mode, etc...
+ */
+
+ Directive.prototype._setupParams = function () {
+ if (!this.params) {
+ return;
+ }
+ var params = this.params;
+ // swap the params array with a fresh object.
+ this.params = Object.create(null);
+ var i = params.length;
+ var key, val, mappedKey;
+ while (i--) {
+ key = hyphenate(params[i]);
+ mappedKey = camelize(key);
+ val = getBindAttr(this.el, key);
+ if (val != null) {
+ // dynamic
+ this._setupParamWatcher(mappedKey, val);
+ } else {
+ // static
+ val = getAttr(this.el, key);
+ if (val != null) {
+ this.params[mappedKey] = val === '' ? true : val;
+ }
+ }
+ }
+ };
+
+ /**
+ * Setup a watcher for a dynamic param.
+ *
+ * @param {String} key
+ * @param {String} expression
+ */
+
+ Directive.prototype._setupParamWatcher = function (key, expression) {
+ var self = this;
+ var called = false;
+ var unwatch = (this._scope || this.vm).$watch(expression, function (val, oldVal) {
+ self.params[key] = val;
+ // since we are in immediate mode,
+ // only call the param change callbacks if this is not the first update.
+ if (called) {
+ var cb = self.paramWatchers && self.paramWatchers[key];
+ if (cb) {
+ cb.call(self, val, oldVal);
+ }
+ } else {
+ called = true;
+ }
+ }, {
+ immediate: true,
+ user: false
+ });(this._paramUnwatchFns || (this._paramUnwatchFns = [])).push(unwatch);
+ };
+
+ /**
+ * Check if the directive is a function caller
+ * and if the expression is a callable one. If both true,
+ * we wrap up the expression and use it as the event
+ * handler.
+ *
+ * e.g. on-click="a++"
+ *
+ * @return {Boolean}
+ */
+
+ Directive.prototype._checkStatement = function () {
+ var expression = this.expression;
+ if (expression && this.acceptStatement && !isSimplePath(expression)) {
+ var fn = parseExpression(expression).get;
+ var scope = this._scope || this.vm;
+ var handler = function handler(e) {
+ scope.$event = e;
+ fn.call(scope, scope);
+ scope.$event = null;
+ };
+ if (this.filters) {
+ handler = scope._applyFilters(handler, null, this.filters);
+ }
+ this.update(handler);
+ return true;
+ }
+ };
+
+ /**
+ * Set the corresponding value with the setter.
+ * This should only be used in two-way directives
+ * e.g. v-model.
+ *
+ * @param {*} value
+ * @public
+ */
+
+ Directive.prototype.set = function (value) {
+ /* istanbul ignore else */
+ if (this.twoWay) {
+ this._withLock(function () {
+ this._watcher.set(value);
+ });
+ } else if ('development' !== 'production') {
+ warn('Directive.set() can only be used inside twoWay' + 'directives.');
+ }
+ };
+
+ /**
+ * Execute a function while preventing that function from
+ * triggering updates on this directive instance.
+ *
+ * @param {Function} fn
+ */
+
+ Directive.prototype._withLock = function (fn) {
+ var self = this;
+ self._locked = true;
+ fn.call(self);
+ nextTick(function () {
+ self._locked = false;
+ });
+ };
+
+ /**
+ * Convenience method that attaches a DOM event listener
+ * to the directive element and autometically tears it down
+ * during unbind.
+ *
+ * @param {String} event
+ * @param {Function} handler
+ * @param {Boolean} [useCapture]
+ */
+
+ Directive.prototype.on = function (event, handler, useCapture) {
+ on(this.el, event, handler, useCapture);(this._listeners || (this._listeners = [])).push([event, handler]);
+ };
+
+ /**
+ * Teardown the watcher and call unbind.
+ */
+
+ Directive.prototype._teardown = function () {
+ if (this._bound) {
+ this._bound = false;
+ if (this.unbind) {
+ this.unbind();
+ }
+ if (this._watcher) {
+ this._watcher.teardown();
+ }
+ var listeners = this._listeners;
+ var i;
+ if (listeners) {
+ i = listeners.length;
+ while (i--) {
+ off(this.el, listeners[i][0], listeners[i][1]);
+ }
+ }
+ var unwatchFns = this._paramUnwatchFns;
+ if (unwatchFns) {
+ i = unwatchFns.length;
+ while (i--) {
+ unwatchFns[i]();
+ }
+ }
+ if ('development' !== 'production' && this.el) {
+ this.el._vue_directives.$remove(this);
+ }
+ this.vm = this.el = this._watcher = this._listeners = null;
+ }
+ };
+
+ function lifecycleMixin (Vue) {
+ /**
+ * Update v-ref for component.
+ *
+ * @param {Boolean} remove
+ */
+
+ Vue.prototype._updateRef = function (remove) {
+ var ref = this.$options._ref;
+ if (ref) {
+ var refs = (this._scope || this._context).$refs;
+ if (remove) {
+ if (refs[ref] === this) {
+ refs[ref] = null;
+ }
+ } else {
+ refs[ref] = this;
+ }
+ }
+ };
+
+ /**
+ * Transclude, compile and link element.
+ *
+ * If a pre-compiled linker is available, that means the
+ * passed in element will be pre-transcluded and compiled
+ * as well - all we need to do is to call the linker.
+ *
+ * Otherwise we need to call transclude/compile/link here.
+ *
+ * @param {Element} el
+ */
+
+ Vue.prototype._compile = function (el) {
+ var options = this.$options;
+
+ // transclude and init element
+ // transclude can potentially replace original
+ // so we need to keep reference; this step also injects
+ // the template and caches the original attributes
+ // on the container node and replacer node.
+ var original = el;
+ el = transclude(el, options);
+ this._initElement(el);
+
+ // handle v-pre on root node (#2026)
+ if (el.nodeType === 1 && getAttr(el, 'v-pre') !== null) {
+ return;
+ }
+
+ // root is always compiled per-instance, because
+ // container attrs and props can be different every time.
+ var contextOptions = this._context && this._context.$options;
+ var rootLinker = compileRoot(el, options, contextOptions);
+
+ // resolve slot distribution
+ resolveSlots(this, options._content);
+
+ // compile and link the rest
+ var contentLinkFn;
+ var ctor = this.constructor;
+ // component compilation can be cached
+ // as long as it's not using inline-template
+ if (options._linkerCachable) {
+ contentLinkFn = ctor.linker;
+ if (!contentLinkFn) {
+ contentLinkFn = ctor.linker = compile(el, options);
+ }
+ }
+
+ // link phase
+ // make sure to link root with prop scope!
+ var rootUnlinkFn = rootLinker(this, el, this._scope);
+ var contentUnlinkFn = contentLinkFn ? contentLinkFn(this, el) : compile(el, options)(this, el);
+
+ // register composite unlink function
+ // to be called during instance destruction
+ this._unlinkFn = function () {
+ rootUnlinkFn();
+ // passing destroying: true to avoid searching and
+ // splicing the directives
+ contentUnlinkFn(true);
+ };
+
+ // finally replace original
+ if (options.replace) {
+ replace(original, el);
+ }
+
+ this._isCompiled = true;
+ this._callHook('compiled');
+ };
+
+ /**
+ * Initialize instance element. Called in the public
+ * $mount() method.
+ *
+ * @param {Element} el
+ */
+
+ Vue.prototype._initElement = function (el) {
+ if (isFragment(el)) {
+ this._isFragment = true;
+ this.$el = this._fragmentStart = el.firstChild;
+ this._fragmentEnd = el.lastChild;
+ // set persisted text anchors to empty
+ if (this._fragmentStart.nodeType === 3) {
+ this._fragmentStart.data = this._fragmentEnd.data = '';
+ }
+ this._fragment = el;
+ } else {
+ this.$el = el;
+ }
+ this.$el.__vue__ = this;
+ this._callHook('beforeCompile');
+ };
+
+ /**
+ * Create and bind a directive to an element.
+ *
+ * @param {Object} descriptor - parsed directive descriptor
+ * @param {Node} node - target node
+ * @param {Vue} [host] - transclusion host component
+ * @param {Object} [scope] - v-for scope
+ * @param {Fragment} [frag] - owner fragment
+ */
+
+ Vue.prototype._bindDir = function (descriptor, node, host, scope, frag) {
+ this._directives.push(new Directive(descriptor, this, node, host, scope, frag));
+ };
+
+ /**
+ * Teardown an instance, unobserves the data, unbind all the
+ * directives, turn off all the event listeners, etc.
+ *
+ * @param {Boolean} remove - whether to remove the DOM node.
+ * @param {Boolean} deferCleanup - if true, defer cleanup to
+ * be called later
+ */
+
+ Vue.prototype._destroy = function (remove, deferCleanup) {
+ if (this._isBeingDestroyed) {
+ if (!deferCleanup) {
+ this._cleanup();
+ }
+ return;
+ }
+
+ var destroyReady;
+ var pendingRemoval;
+
+ var self = this;
+ // Cleanup should be called either synchronously or asynchronoysly as
+ // callback of this.$remove(), or if remove and deferCleanup are false.
+ // In any case it should be called after all other removing, unbinding and
+ // turning of is done
+ var cleanupIfPossible = function cleanupIfPossible() {
+ if (destroyReady && !pendingRemoval && !deferCleanup) {
+ self._cleanup();
+ }
+ };
+
+ // remove DOM element
+ if (remove && this.$el) {
+ pendingRemoval = true;
+ this.$remove(function () {
+ pendingRemoval = false;
+ cleanupIfPossible();
+ });
+ }
+
+ this._callHook('beforeDestroy');
+ this._isBeingDestroyed = true;
+ var i;
+ // remove self from parent. only necessary
+ // if parent is not being destroyed as well.
+ var parent = this.$parent;
+ if (parent && !parent._isBeingDestroyed) {
+ parent.$children.$remove(this);
+ // unregister ref (remove: true)
+ this._updateRef(true);
+ }
+ // destroy all children.
+ i = this.$children.length;
+ while (i--) {
+ this.$children[i].$destroy();
+ }
+ // teardown props
+ if (this._propsUnlinkFn) {
+ this._propsUnlinkFn();
+ }
+ // teardown all directives. this also tearsdown all
+ // directive-owned watchers.
+ if (this._unlinkFn) {
+ this._unlinkFn();
+ }
+ i = this._watchers.length;
+ while (i--) {
+ this._watchers[i].teardown();
+ }
+ // remove reference to self on $el
+ if (this.$el) {
+ this.$el.__vue__ = null;
+ }
+
+ destroyReady = true;
+ cleanupIfPossible();
+ };
+
+ /**
+ * Clean up to ensure garbage collection.
+ * This is called after the leave transition if there
+ * is any.
+ */
+
+ Vue.prototype._cleanup = function () {
+ if (this._isDestroyed) {
+ return;
+ }
+ // remove self from owner fragment
+ // do it in cleanup so that we can call $destroy with
+ // defer right when a fragment is about to be removed.
+ if (this._frag) {
+ this._frag.children.$remove(this);
+ }
+ // remove reference from data ob
+ // frozen object may not have observer.
+ if (this._data && this._data.__ob__) {
+ this._data.__ob__.removeVm(this);
+ }
+ // Clean up references to private properties and other
+ // instances. preserve reference to _data so that proxy
+ // accessors still work. The only potential side effect
+ // here is that mutating the instance after it's destroyed
+ // may affect the state of other components that are still
+ // observing the same object, but that seems to be a
+ // reasonable responsibility for the user rather than
+ // always throwing an error on them.
+ this.$el = this.$parent = this.$root = this.$children = this._watchers = this._context = this._scope = this._directives = null;
+ // call the last hook...
+ this._isDestroyed = true;
+ this._callHook('destroyed');
+ // turn off all instance listeners.
+ this.$off();
+ };
+ }
+
+ function miscMixin (Vue) {
+ /**
+ * Apply a list of filter (descriptors) to a value.
+ * Using plain for loops here because this will be called in
+ * the getter of any watcher with filters so it is very
+ * performance sensitive.
+ *
+ * @param {*} value
+ * @param {*} [oldValue]
+ * @param {Array} filters
+ * @param {Boolean} write
+ * @return {*}
+ */
+
+ Vue.prototype._applyFilters = function (value, oldValue, filters, write) {
+ var filter, fn, args, arg, offset, i, l, j, k;
+ for (i = 0, l = filters.length; i < l; i++) {
+ filter = filters[write ? l - i - 1 : i];
+ fn = resolveAsset(this.$options, 'filters', filter.name, true);
+ if (!fn) continue;
+ fn = write ? fn.write : fn.read || fn;
+ if (typeof fn !== 'function') continue;
+ args = write ? [value, oldValue] : [value];
+ offset = write ? 2 : 1;
+ if (filter.args) {
+ for (j = 0, k = filter.args.length; j < k; j++) {
+ arg = filter.args[j];
+ args[j + offset] = arg.dynamic ? this.$get(arg.value) : arg.value;
+ }
+ }
+ value = fn.apply(this, args);
+ }
+ return value;
+ };
+
+ /**
+ * Resolve a component, depending on whether the component
+ * is defined normally or using an async factory function.
+ * Resolves synchronously if already resolved, otherwise
+ * resolves asynchronously and caches the resolved
+ * constructor on the factory.
+ *
+ * @param {String|Function} value
+ * @param {Function} cb
+ */
+
+ Vue.prototype._resolveComponent = function (value, cb) {
+ var factory;
+ if (typeof value === 'function') {
+ factory = value;
+ } else {
+ factory = resolveAsset(this.$options, 'components', value, true);
+ }
+ /* istanbul ignore if */
+ if (!factory) {
+ return;
+ }
+ // async component factory
+ if (!factory.options) {
+ if (factory.resolved) {
+ // cached
+ cb(factory.resolved);
+ } else if (factory.requested) {
+ // pool callbacks
+ factory.pendingCallbacks.push(cb);
+ } else {
+ factory.requested = true;
+ var cbs = factory.pendingCallbacks = [cb];
+ factory.call(this, function resolve(res) {
+ if (isPlainObject(res)) {
+ res = Vue.extend(res);
+ }
+ // cache resolved
+ factory.resolved = res;
+ // invoke callbacks
+ for (var i = 0, l = cbs.length; i < l; i++) {
+ cbs[i](res);
+ }
+ }, function reject(reason) {
+ 'development' !== 'production' && warn('Failed to resolve async component' + (typeof value === 'string' ? ': ' + value : '') + '. ' + (reason ? '\nReason: ' + reason : ''));
+ });
+ }
+ } else {
+ // normal component
+ cb(factory);
+ }
+ };
+ }
+
+ var filterRE$1 = /[^|]\|[^|]/;
+
+ function dataAPI (Vue) {
+ /**
+ * Get the value from an expression on this vm.
+ *
+ * @param {String} exp
+ * @param {Boolean} [asStatement]
+ * @return {*}
+ */
+
+ Vue.prototype.$get = function (exp, asStatement) {
+ var res = parseExpression(exp);
+ if (res) {
+ if (asStatement) {
+ var self = this;
+ return function statementHandler() {
+ self.$arguments = toArray(arguments);
+ var result = res.get.call(self, self);
+ self.$arguments = null;
+ return result;
+ };
+ } else {
+ try {
+ return res.get.call(this, this);
+ } catch (e) {}
+ }
+ }
+ };
+
+ /**
+ * Set the value from an expression on this vm.
+ * The expression must be a valid left-hand
+ * expression in an assignment.
+ *
+ * @param {String} exp
+ * @param {*} val
+ */
+
+ Vue.prototype.$set = function (exp, val) {
+ var res = parseExpression(exp, true);
+ if (res && res.set) {
+ res.set.call(this, this, val);
+ }
+ };
+
+ /**
+ * Delete a property on the VM
+ *
+ * @param {String} key
+ */
+
+ Vue.prototype.$delete = function (key) {
+ del(this._data, key);
+ };
+
+ /**
+ * Watch an expression, trigger callback when its
+ * value changes.
+ *
+ * @param {String|Function} expOrFn
+ * @param {Function} cb
+ * @param {Object} [options]
+ * - {Boolean} deep
+ * - {Boolean} immediate
+ * @return {Function} - unwatchFn
+ */
+
+ Vue.prototype.$watch = function (expOrFn, cb, options) {
+ var vm = this;
+ var parsed;
+ if (typeof expOrFn === 'string') {
+ parsed = parseDirective(expOrFn);
+ expOrFn = parsed.expression;
+ }
+ var watcher = new Watcher(vm, expOrFn, cb, {
+ deep: options && options.deep,
+ sync: options && options.sync,
+ filters: parsed && parsed.filters,
+ user: !options || options.user !== false
+ });
+ if (options && options.immediate) {
+ cb.call(vm, watcher.value);
+ }
+ return function unwatchFn() {
+ watcher.teardown();
+ };
+ };
+
+ /**
+ * Evaluate a text directive, including filters.
+ *
+ * @param {String} text
+ * @param {Boolean} [asStatement]
+ * @return {String}
+ */
+
+ Vue.prototype.$eval = function (text, asStatement) {
+ // check for filters.
+ if (filterRE$1.test(text)) {
+ var dir = parseDirective(text);
+ // the filter regex check might give false positive
+ // for pipes inside strings, so it's possible that
+ // we don't get any filters here
+ var val = this.$get(dir.expression, asStatement);
+ return dir.filters ? this._applyFilters(val, null, dir.filters) : val;
+ } else {
+ // no filter
+ return this.$get(text, asStatement);
+ }
+ };
+
+ /**
+ * Interpolate a piece of template text.
+ *
+ * @param {String} text
+ * @return {String}
+ */
+
+ Vue.prototype.$interpolate = function (text) {
+ var tokens = parseText(text);
+ var vm = this;
+ if (tokens) {
+ if (tokens.length === 1) {
+ return vm.$eval(tokens[0].value) + '';
+ } else {
+ return tokens.map(function (token) {
+ return token.tag ? vm.$eval(token.value) : token.value;
+ }).join('');
+ }
+ } else {
+ return text;
+ }
+ };
+
+ /**
+ * Log instance data as a plain JS object
+ * so that it is easier to inspect in console.
+ * This method assumes console is available.
+ *
+ * @param {String} [path]
+ */
+
+ Vue.prototype.$log = function (path) {
+ var data = path ? getPath(this._data, path) : this._data;
+ if (data) {
+ data = clean(data);
+ }
+ // include computed fields
+ if (!path) {
+ var key;
+ for (key in this.$options.computed) {
+ data[key] = clean(this[key]);
+ }
+ if (this._props) {
+ for (key in this._props) {
+ data[key] = clean(this[key]);
+ }
+ }
+ }
+ console.log(data);
+ };
+
+ /**
+ * "clean" a getter/setter converted object into a plain
+ * object copy.
+ *
+ * @param {Object} - obj
+ * @return {Object}
+ */
+
+ function clean(obj) {
+ return JSON.parse(JSON.stringify(obj));
+ }
+ }
+
+ function domAPI (Vue) {
+ /**
+ * Convenience on-instance nextTick. The callback is
+ * auto-bound to the instance, and this avoids component
+ * modules having to rely on the global Vue.
+ *
+ * @param {Function} fn
+ */
+
+ Vue.prototype.$nextTick = function (fn) {
+ nextTick(fn, this);
+ };
+
+ /**
+ * Append instance to target
+ *
+ * @param {Node} target
+ * @param {Function} [cb]
+ * @param {Boolean} [withTransition] - defaults to true
+ */
+
+ Vue.prototype.$appendTo = function (target, cb, withTransition) {
+ return insert(this, target, cb, withTransition, append, appendWithTransition);
+ };
+
+ /**
+ * Prepend instance to target
+ *
+ * @param {Node} target
+ * @param {Function} [cb]
+ * @param {Boolean} [withTransition] - defaults to true
+ */
+
+ Vue.prototype.$prependTo = function (target, cb, withTransition) {
+ target = query(target);
+ if (target.hasChildNodes()) {
+ this.$before(target.firstChild, cb, withTransition);
+ } else {
+ this.$appendTo(target, cb, withTransition);
+ }
+ return this;
+ };
+
+ /**
+ * Insert instance before target
+ *
+ * @param {Node} target
+ * @param {Function} [cb]
+ * @param {Boolean} [withTransition] - defaults to true
+ */
+
+ Vue.prototype.$before = function (target, cb, withTransition) {
+ return insert(this, target, cb, withTransition, beforeWithCb, beforeWithTransition);
+ };
+
+ /**
+ * Insert instance after target
+ *
+ * @param {Node} target
+ * @param {Function} [cb]
+ * @param {Boolean} [withTransition] - defaults to true
+ */
+
+ Vue.prototype.$after = function (target, cb, withTransition) {
+ target = query(target);
+ if (target.nextSibling) {
+ this.$before(target.nextSibling, cb, withTransition);
+ } else {
+ this.$appendTo(target.parentNode, cb, withTransition);
+ }
+ return this;
+ };
+
+ /**
+ * Remove instance from DOM
+ *
+ * @param {Function} [cb]
+ * @param {Boolean} [withTransition] - defaults to true
+ */
+
+ Vue.prototype.$remove = function (cb, withTransition) {
+ if (!this.$el.parentNode) {
+ return cb && cb();
+ }
+ var inDocument = this._isAttached && inDoc(this.$el);
+ // if we are not in document, no need to check
+ // for transitions
+ if (!inDocument) withTransition = false;
+ var self = this;
+ var realCb = function realCb() {
+ if (inDocument) self._callHook('detached');
+ if (cb) cb();
+ };
+ if (this._isFragment) {
+ removeNodeRange(this._fragmentStart, this._fragmentEnd, this, this._fragment, realCb);
+ } else {
+ var op = withTransition === false ? removeWithCb : removeWithTransition;
+ op(this.$el, this, realCb);
+ }
+ return this;
+ };
+
+ /**
+ * Shared DOM insertion function.
+ *
+ * @param {Vue} vm
+ * @param {Element} target
+ * @param {Function} [cb]
+ * @param {Boolean} [withTransition]
+ * @param {Function} op1 - op for non-transition insert
+ * @param {Function} op2 - op for transition insert
+ * @return vm
+ */
+
+ function insert(vm, target, cb, withTransition, op1, op2) {
+ target = query(target);
+ var targetIsDetached = !inDoc(target);
+ var op = withTransition === false || targetIsDetached ? op1 : op2;
+ var shouldCallHook = !targetIsDetached && !vm._isAttached && !inDoc(vm.$el);
+ if (vm._isFragment) {
+ mapNodeRange(vm._fragmentStart, vm._fragmentEnd, function (node) {
+ op(node, target, vm);
+ });
+ cb && cb();
+ } else {
+ op(vm.$el, target, vm, cb);
+ }
+ if (shouldCallHook) {
+ vm._callHook('attached');
+ }
+ return vm;
+ }
+
+ /**
+ * Check for selectors
+ *
+ * @param {String|Element} el
+ */
+
+ function query(el) {
+ return typeof el === 'string' ? document.querySelector(el) : el;
+ }
+
+ /**
+ * Append operation that takes a callback.
+ *
+ * @param {Node} el
+ * @param {Node} target
+ * @param {Vue} vm - unused
+ * @param {Function} [cb]
+ */
+
+ function append(el, target, vm, cb) {
+ target.appendChild(el);
+ if (cb) cb();
+ }
+
+ /**
+ * InsertBefore operation that takes a callback.
+ *
+ * @param {Node} el
+ * @param {Node} target
+ * @param {Vue} vm - unused
+ * @param {Function} [cb]
+ */
+
+ function beforeWithCb(el, target, vm, cb) {
+ before(el, target);
+ if (cb) cb();
+ }
+
+ /**
+ * Remove operation that takes a callback.
+ *
+ * @param {Node} el
+ * @param {Vue} vm - unused
+ * @param {Function} [cb]
+ */
+
+ function removeWithCb(el, vm, cb) {
+ remove(el);
+ if (cb) cb();
+ }
+ }
+
+ function eventsAPI (Vue) {
+ /**
+ * Listen on the given `event` with `fn`.
+ *
+ * @param {String} event
+ * @param {Function} fn
+ */
+
+ Vue.prototype.$on = function (event, fn) {
+ (this._events[event] || (this._events[event] = [])).push(fn);
+ modifyListenerCount(this, event, 1);
+ return this;
+ };
+
+ /**
+ * Adds an `event` listener that will be invoked a single
+ * time then automatically removed.
+ *
+ * @param {String} event
+ * @param {Function} fn
+ */
+
+ Vue.prototype.$once = function (event, fn) {
+ var self = this;
+ function on() {
+ self.$off(event, on);
+ fn.apply(this, arguments);
+ }
+ on.fn = fn;
+ this.$on(event, on);
+ return this;
+ };
+
+ /**
+ * Remove the given callback for `event` or all
+ * registered callbacks.
+ *
+ * @param {String} event
+ * @param {Function} fn
+ */
+
+ Vue.prototype.$off = function (event, fn) {
+ var cbs;
+ // all
+ if (!arguments.length) {
+ if (this.$parent) {
+ for (event in this._events) {
+ cbs = this._events[event];
+ if (cbs) {
+ modifyListenerCount(this, event, -cbs.length);
+ }
+ }
+ }
+ this._events = {};
+ return this;
+ }
+ // specific event
+ cbs = this._events[event];
+ if (!cbs) {
+ return this;
+ }
+ if (arguments.length === 1) {
+ modifyListenerCount(this, event, -cbs.length);
+ this._events[event] = null;
+ return this;
+ }
+ // specific handler
+ var cb;
+ var i = cbs.length;
+ while (i--) {
+ cb = cbs[i];
+ if (cb === fn || cb.fn === fn) {
+ modifyListenerCount(this, event, -1);
+ cbs.splice(i, 1);
+ break;
+ }
+ }
+ return this;
+ };
+
+ /**
+ * Trigger an event on self.
+ *
+ * @param {String|Object} event
+ * @return {Boolean} shouldPropagate
+ */
+
+ Vue.prototype.$emit = function (event) {
+ var isSource = typeof event === 'string';
+ event = isSource ? event : event.name;
+ var cbs = this._events[event];
+ var shouldPropagate = isSource || !cbs;
+ if (cbs) {
+ cbs = cbs.length > 1 ? toArray(cbs) : cbs;
+ // this is a somewhat hacky solution to the question raised
+ // in #2102: for an inline component listener like <comp @test="doThis">,
+ // the propagation handling is somewhat broken. Therefore we
+ // need to treat these inline callbacks differently.
+ var hasParentCbs = isSource && cbs.some(function (cb) {
+ return cb._fromParent;
+ });
+ if (hasParentCbs) {
+ shouldPropagate = false;
+ }
+ var args = toArray(arguments, 1);
+ for (var i = 0, l = cbs.length; i < l; i++) {
+ var cb = cbs[i];
+ var res = cb.apply(this, args);
+ if (res === true && (!hasParentCbs || cb._fromParent)) {
+ shouldPropagate = true;
+ }
+ }
+ }
+ return shouldPropagate;
+ };
+
+ /**
+ * Recursively broadcast an event to all children instances.
+ *
+ * @param {String|Object} event
+ * @param {...*} additional arguments
+ */
+
+ Vue.prototype.$broadcast = function (event) {
+ var isSource = typeof event === 'string';
+ event = isSource ? event : event.name;
+ // if no child has registered for this event,
+ // then there's no need to broadcast.
+ if (!this._eventsCount[event]) return;
+ var children = this.$children;
+ var args = toArray(arguments);
+ if (isSource) {
+ // use object event to indicate non-source emit
+ // on children
+ args[0] = { name: event, source: this };
+ }
+ for (var i = 0, l = children.length; i < l; i++) {
+ var child = children[i];
+ var shouldPropagate = child.$emit.apply(child, args);
+ if (shouldPropagate) {
+ child.$broadcast.apply(child, args);
+ }
+ }
+ return this;
+ };
+
+ /**
+ * Recursively propagate an event up the parent chain.
+ *
+ * @param {String} event
+ * @param {...*} additional arguments
+ */
+
+ Vue.prototype.$dispatch = function (event) {
+ var shouldPropagate = this.$emit.apply(this, arguments);
+ if (!shouldPropagate) return;
+ var parent = this.$parent;
+ var args = toArray(arguments);
+ // use object event to indicate non-source emit
+ // on parents
+ args[0] = { name: event, source: this };
+ while (parent) {
+ shouldPropagate = parent.$emit.apply(parent, args);
+ parent = shouldPropagate ? parent.$parent : null;
+ }
+ return this;
+ };
+
+ /**
+ * Modify the listener counts on all parents.
+ * This bookkeeping allows $broadcast to return early when
+ * no child has listened to a certain event.
+ *
+ * @param {Vue} vm
+ * @param {String} event
+ * @param {Number} count
+ */
+
+ var hookRE = /^hook:/;
+ function modifyListenerCount(vm, event, count) {
+ var parent = vm.$parent;
+ // hooks do not get broadcasted so no need
+ // to do bookkeeping for them
+ if (!parent || !count || hookRE.test(event)) return;
+ while (parent) {
+ parent._eventsCount[event] = (parent._eventsCount[event] || 0) + count;
+ parent = parent.$parent;
+ }
+ }
+ }
+
+ function lifecycleAPI (Vue) {
+ /**
+ * Set instance target element and kick off the compilation
+ * process. The passed in `el` can be a selector string, an
+ * existing Element, or a DocumentFragment (for block
+ * instances).
+ *
+ * @param {Element|DocumentFragment|string} el
+ * @public
+ */
+
+ Vue.prototype.$mount = function (el) {
+ if (this._isCompiled) {
+ 'development' !== 'production' && warn('$mount() should be called only once.', this);
+ return;
+ }
+ el = query(el);
+ if (!el) {
+ el = document.createElement('div');
+ }
+ this._compile(el);
+ this._initDOMHooks();
+ if (inDoc(this.$el)) {
+ this._callHook('attached');
+ ready.call(this);
+ } else {
+ this.$once('hook:attached', ready);
+ }
+ return this;
+ };
+
+ /**
+ * Mark an instance as ready.
+ */
+
+ function ready() {
+ this._isAttached = true;
+ this._isReady = true;
+ this._callHook('ready');
+ }
+
+ /**
+ * Teardown the instance, simply delegate to the internal
+ * _destroy.
+ *
+ * @param {Boolean} remove
+ * @param {Boolean} deferCleanup
+ */
+
+ Vue.prototype.$destroy = function (remove, deferCleanup) {
+ this._destroy(remove, deferCleanup);
+ };
+
+ /**
+ * Partially compile a piece of DOM and return a
+ * decompile function.
+ *
+ * @param {Element|DocumentFragment} el
+ * @param {Vue} [host]
+ * @param {Object} [scope]
+ * @param {Fragment} [frag]
+ * @return {Function}
+ */
+
+ Vue.prototype.$compile = function (el, host, scope, frag) {
+ return compile(el, this.$options, true)(this, el, host, scope, frag);
+ };
+ }
+
+ /**
+ * The exposed Vue constructor.
+ *
+ * API conventions:
+ * - public API methods/properties are prefixed with `$`
+ * - internal methods/properties are prefixed with `_`
+ * - non-prefixed properties are assumed to be proxied user
+ * data.
+ *
+ * @constructor
+ * @param {Object} [options]
+ * @public
+ */
+
+ function Vue(options) {
+ this._init(options);
+ }
+
+ // install internals
+ initMixin(Vue);
+ stateMixin(Vue);
+ eventsMixin(Vue);
+ lifecycleMixin(Vue);
+ miscMixin(Vue);
+
+ // install instance APIs
+ dataAPI(Vue);
+ domAPI(Vue);
+ eventsAPI(Vue);
+ lifecycleAPI(Vue);
+
+ var slot = {
+
+ priority: SLOT,
+ params: ['name'],
+
+ bind: function bind() {
+ // this was resolved during component transclusion
+ var name = this.params.name || 'default';
+ var content = this.vm._slotContents && this.vm._slotContents[name];
+ if (!content || !content.hasChildNodes()) {
+ this.fallback();
+ } else {
+ this.compile(content.cloneNode(true), this.vm._context, this.vm);
+ }
+ },
+
+ compile: function compile(content, context, host) {
+ if (content && context) {
+ if (this.el.hasChildNodes() && content.childNodes.length === 1 && content.childNodes[0].nodeType === 1 && content.childNodes[0].hasAttribute('v-if')) {
+ // if the inserted slot has v-if
+ // inject fallback content as the v-else
+ var elseBlock = document.createElement('template');
+ elseBlock.setAttribute('v-else', '');
+ elseBlock.innerHTML = this.el.innerHTML;
+ // the else block should be compiled in child scope
+ elseBlock._context = this.vm;
+ content.appendChild(elseBlock);
+ }
+ var scope = host ? host._scope : this._scope;
+ this.unlink = context.$compile(content, host, scope, this._frag);
+ }
+ if (content) {
+ replace(this.el, content);
+ } else {
+ remove(this.el);
+ }
+ },
+
+ fallback: function fallback() {
+ this.compile(extractContent(this.el, true), this.vm);
+ },
+
+ unbind: function unbind() {
+ if (this.unlink) {
+ this.unlink();
+ }
+ }
+ };
+
+ var partial = {
+
+ priority: PARTIAL,
+
+ params: ['name'],
+
+ // watch changes to name for dynamic partials
+ paramWatchers: {
+ name: function name(value) {
+ vIf.remove.call(this);
+ if (value) {
+ this.insert(value);
+ }
+ }
+ },
+
+ bind: function bind() {
+ this.anchor = createAnchor('v-partial');
+ replace(this.el, this.anchor);
+ this.insert(this.params.name);
+ },
+
+ insert: function insert(id) {
+ var partial = resolveAsset(this.vm.$options, 'partials', id, true);
+ if (partial) {
+ this.factory = new FragmentFactory(this.vm, partial);
+ vIf.insert.call(this);
+ }
+ },
+
+ unbind: function unbind() {
+ if (this.frag) {
+ this.frag.destroy();
+ }
+ }
+ };
+
+ var elementDirectives = {
+ slot: slot,
+ partial: partial
+ };
+
+ var convertArray = vFor._postProcess;
+
+ /**
+ * Limit filter for arrays
+ *
+ * @param {Number} n
+ * @param {Number} offset (Decimal expected)
+ */
+
+ function limitBy(arr, n, offset) {
+ offset = offset ? parseInt(offset, 10) : 0;
+ n = toNumber(n);
+ return typeof n === 'number' ? arr.slice(offset, offset + n) : arr;
+ }
+
+ /**
+ * Filter filter for arrays
+ *
+ * @param {String} search
+ * @param {String} [delimiter]
+ * @param {String} ...dataKeys
+ */
+
+ function filterBy(arr, search, delimiter) {
+ arr = convertArray(arr);
+ if (search == null) {
+ return arr;
+ }
+ if (typeof search === 'function') {
+ return arr.filter(search);
+ }
+ // cast to lowercase string
+ search = ('' + search).toLowerCase();
+ // allow optional `in` delimiter
+ // because why not
+ var n = delimiter === 'in' ? 3 : 2;
+ // extract and flatten keys
+ var keys = Array.prototype.concat.apply([], toArray(arguments, n));
+ var res = [];
+ var item, key, val, j;
+ for (var i = 0, l = arr.length; i < l; i++) {
+ item = arr[i];
+ val = item && item.$value || item;
+ j = keys.length;
+ if (j) {
+ while (j--) {
+ key = keys[j];
+ if (key === '$key' && contains(item.$key, search) || contains(getPath(val, key), search)) {
+ res.push(item);
+ break;
+ }
+ }
+ } else if (contains(item, search)) {
+ res.push(item);
+ }
+ }
+ return res;
+ }
+
+ /**
+ * Filter filter for arrays
+ *
+ * @param {String|Array<String>|Function} ...sortKeys
+ * @param {Number} [order]
+ */
+
+ function orderBy(arr) {
+ var comparator = null;
+ var sortKeys = undefined;
+ arr = convertArray(arr);
+
+ // determine order (last argument)
+ var args = toArray(arguments, 1);
+ var order = args[args.length - 1];
+ if (typeof order === 'number') {
+ order = order < 0 ? -1 : 1;
+ args = args.length > 1 ? args.slice(0, -1) : args;
+ } else {
+ order = 1;
+ }
+
+ // determine sortKeys & comparator
+ var firstArg = args[0];
+ if (!firstArg) {
+ return arr;
+ } else if (typeof firstArg === 'function') {
+ // custom comparator
+ comparator = function (a, b) {
+ return firstArg(a, b) * order;
+ };
+ } else {
+ // string keys. flatten first
+ sortKeys = Array.prototype.concat.apply([], args);
+ comparator = function (a, b, i) {
+ i = i || 0;
+ return i >= sortKeys.length - 1 ? baseCompare(a, b, i) : baseCompare(a, b, i) || comparator(a, b, i + 1);
+ };
+ }
+
+ function baseCompare(a, b, sortKeyIndex) {
+ var sortKey = sortKeys[sortKeyIndex];
+ if (sortKey) {
+ if (sortKey !== '$key') {
+ if (isObject(a) && '$value' in a) a = a.$value;
+ if (isObject(b) && '$value' in b) b = b.$value;
+ }
+ a = isObject(a) ? getPath(a, sortKey) : a;
+ b = isObject(b) ? getPath(b, sortKey) : b;
+ }
+ return a === b ? 0 : a > b ? order : -order;
+ }
+
+ // sort on a copy to avoid mutating original array
+ return arr.slice().sort(comparator);
+ }
+
+ /**
+ * String contain helper
+ *
+ * @param {*} val
+ * @param {String} search
+ */
+
+ function contains(val, search) {
+ var i;
+ if (isPlainObject(val)) {
+ var keys = Object.keys(val);
+ i = keys.length;
+ while (i--) {
+ if (contains(val[keys[i]], search)) {
+ return true;
+ }
+ }
+ } else if (isArray(val)) {
+ i = val.length;
+ while (i--) {
+ if (contains(val[i], search)) {
+ return true;
+ }
+ }
+ } else if (val != null) {
+ return val.toString().toLowerCase().indexOf(search) > -1;
+ }
+ }
+
+ var digitsRE = /(\d{3})(?=\d)/g;
+
+ // asset collections must be a plain object.
+ var filters = {
+
+ orderBy: orderBy,
+ filterBy: filterBy,
+ limitBy: limitBy,
+
+ /**
+ * Stringify value.
+ *
+ * @param {Number} indent
+ */
+
+ json: {
+ read: function read(value, indent) {
+ return typeof value === 'string' ? value : JSON.stringify(value, null, arguments.length > 1 ? indent : 2);
+ },
+ write: function write(value) {
+ try {
+ return JSON.parse(value);
+ } catch (e) {
+ return value;
+ }
+ }
+ },
+
+ /**
+ * 'abc' => 'Abc'
+ */
+
+ capitalize: function capitalize(value) {
+ if (!value && value !== 0) return '';
+ value = value.toString();
+ return value.charAt(0).toUpperCase() + value.slice(1);
+ },
+
+ /**
+ * 'abc' => 'ABC'
+ */
+
+ uppercase: function uppercase(value) {
+ return value || value === 0 ? value.toString().toUpperCase() : '';
+ },
+
+ /**
+ * 'AbC' => 'abc'
+ */
+
+ lowercase: function lowercase(value) {
+ return value || value === 0 ? value.toString().toLowerCase() : '';
+ },
+
+ /**
+ * 12345 => $12,345.00
+ *
+ * @param {String} sign
+ * @param {Number} decimals Decimal places
+ */
+
+ currency: function currency(value, _currency, decimals) {
+ value = parseFloat(value);
+ if (!isFinite(value) || !value && value !== 0) return '';
+ _currency = _currency != null ? _currency : '$';
+ decimals = decimals != null ? decimals : 2;
+ var stringified = Math.abs(value).toFixed(decimals);
+ var _int = decimals ? stringified.slice(0, -1 - decimals) : stringified;
+ var i = _int.length % 3;
+ var head = i > 0 ? _int.slice(0, i) + (_int.length > 3 ? ',' : '') : '';
+ var _float = decimals ? stringified.slice(-1 - decimals) : '';
+ var sign = value < 0 ? '-' : '';
+ return sign + _currency + head + _int.slice(i).replace(digitsRE, '$1,') + _float;
+ },
+
+ /**
+ * 'item' => 'items'
+ *
+ * @params
+ * an array of strings corresponding to
+ * the single, double, triple ... forms of the word to
+ * be pluralized. When the number to be pluralized
+ * exceeds the length of the args, it will use the last
+ * entry in the array.
+ *
+ * e.g. ['single', 'double', 'triple', 'multiple']
+ */
+
+ pluralize: function pluralize(value) {
+ var args = toArray(arguments, 1);
+ var length = args.length;
+ if (length > 1) {
+ var index = value % 10 - 1;
+ return index in args ? args[index] : args[length - 1];
+ } else {
+ return args[0] + (value === 1 ? '' : 's');
+ }
+ },
+
+ /**
+ * Debounce a handler function.
+ *
+ * @param {Function} handler
+ * @param {Number} delay = 300
+ * @return {Function}
+ */
+
+ debounce: function debounce(handler, delay) {
+ if (!handler) return;
+ if (!delay) {
+ delay = 300;
+ }
+ return _debounce(handler, delay);
+ }
+ };
+
+ function installGlobalAPI (Vue) {
+ /**
+ * Vue and every constructor that extends Vue has an
+ * associated options object, which can be accessed during
+ * compilation steps as `this.constructor.options`.
+ *
+ * These can be seen as the default options of every
+ * Vue instance.
+ */
+
+ Vue.options = {
+ directives: directives,
+ elementDirectives: elementDirectives,
+ filters: filters,
+ transitions: {},
+ components: {},
+ partials: {},
+ replace: true
+ };
+
+ /**
+ * Expose useful internals
+ */
+
+ Vue.util = util;
+ Vue.config = config;
+ Vue.set = set;
+ Vue['delete'] = del;
+ Vue.nextTick = nextTick;
+
+ /**
+ * The following are exposed for advanced usage / plugins
+ */
+
+ Vue.compiler = compiler;
+ Vue.FragmentFactory = FragmentFactory;
+ Vue.internalDirectives = internalDirectives;
+ Vue.parsers = {
+ path: path,
+ text: text,
+ template: template,
+ directive: directive,
+ expression: expression
+ };
+
+ /**
+ * Each instance constructor, including Vue, has a unique
+ * cid. This enables us to create wrapped "child
+ * constructors" for prototypal inheritance and cache them.
+ */
+
+ Vue.cid = 0;
+ var cid = 1;
+
+ /**
+ * Class inheritance
+ *
+ * @param {Object} extendOptions
+ */
+
+ Vue.extend = function (extendOptions) {
+ extendOptions = extendOptions || {};
+ var Super = this;
+ var isFirstExtend = Super.cid === 0;
+ if (isFirstExtend && extendOptions._Ctor) {
+ return extendOptions._Ctor;
+ }
+ var name = extendOptions.name || Super.options.name;
+ if ('development' !== 'production') {
+ if (!/^[a-zA-Z][\w-]*$/.test(name)) {
+ warn('Invalid component name: "' + name + '". Component names ' + 'can only contain alphanumeric characaters and the hyphen.');
+ name = null;
+ }
+ }
+ var Sub = createClass(name || 'VueComponent');
+ Sub.prototype = Object.create(Super.prototype);
+ Sub.prototype.constructor = Sub;
+ Sub.cid = cid++;
+ Sub.options = mergeOptions(Super.options, extendOptions);
+ Sub['super'] = Super;
+ // allow further extension
+ Sub.extend = Super.extend;
+ // create asset registers, so extended classes
+ // can have their private assets too.
+ config._assetTypes.forEach(function (type) {
+ Sub[type] = Super[type];
+ });
+ // enable recursive self-lookup
+ if (name) {
+ Sub.options.components[name] = Sub;
+ }
+ // cache constructor
+ if (isFirstExtend) {
+ extendOptions._Ctor = Sub;
+ }
+ return Sub;
+ };
+
+ /**
+ * A function that returns a sub-class constructor with the
+ * given name. This gives us much nicer output when
+ * logging instances in the console.
+ *
+ * @param {String} name
+ * @return {Function}
+ */
+
+ function createClass(name) {
+ /* eslint-disable no-new-func */
+ return new Function('return function ' + classify(name) + ' (options) { this._init(options) }')();
+ /* eslint-enable no-new-func */
+ }
+
+ /**
+ * Plugin system
+ *
+ * @param {Object} plugin
+ */
+
+ Vue.use = function (plugin) {
+ /* istanbul ignore if */
+ if (plugin.installed) {
+ return;
+ }
+ // additional parameters
+ var args = toArray(arguments, 1);
+ args.unshift(this);
+ if (typeof plugin.install === 'function') {
+ plugin.install.apply(plugin, args);
+ } else {
+ plugin.apply(null, args);
+ }
+ plugin.installed = true;
+ return this;
+ };
+
+ /**
+ * Apply a global mixin by merging it into the default
+ * options.
+ */
+
+ Vue.mixin = function (mixin) {
+ Vue.options = mergeOptions(Vue.options, mixin);
+ };
+
+ /**
+ * Create asset registration methods with the following
+ * signature:
+ *
+ * @param {String} id
+ * @param {*} definition
+ */
+
+ config._assetTypes.forEach(function (type) {
+ Vue[type] = function (id, definition) {
+ if (!definition) {
+ return this.options[type + 's'][id];
+ } else {
+ /* istanbul ignore if */
+ if ('development' !== 'production') {
+ if (type === 'component' && (commonTagRE.test(id) || reservedTagRE.test(id))) {
+ warn('Do not use built-in or reserved HTML elements as component ' + 'id: ' + id);
+ }
+ }
+ if (type === 'component' && isPlainObject(definition)) {
+ if (!definition.name) {
+ definition.name = id;
+ }
+ definition = Vue.extend(definition);
+ }
+ this.options[type + 's'][id] = definition;
+ return definition;
+ }
+ };
+ });
+
+ // expose internal transition API
+ extend(Vue.transition, transition);
+ }
+
+ installGlobalAPI(Vue);
+
+ Vue.version = '1.0.26';
+
+ // devtools global hook
+ /* istanbul ignore next */
+ setTimeout(function () {
+ if (config.devtools) {
+ if (devtools) {
+ devtools.emit('init', Vue);
+ } else if ('development' !== 'production' && inBrowser && /Chrome\/\d+/.test(window.navigator.userAgent)) {
+ console.log('Download the Vue Devtools for a better development experience:\n' + 'https://github.com/vuejs/vue-devtools');
+ }
+ }
+ }, 0);
+
+ return Vue;
+
+})); \ No newline at end of file
diff --git a/vendor/assets/javascripts/vue.js.erb b/vendor/assets/javascripts/vue.js.erb
new file mode 100644
index 00000000000..008beb10f4d
--- /dev/null
+++ b/vendor/assets/javascripts/vue.js.erb
@@ -0,0 +1,2 @@
+<% type = Rails.env.development? ? 'full' : 'min' %>
+<%= File.read(Rails.root.join("vendor/assets/javascripts/vue.#{type}.js")) %>
diff --git a/vendor/assets/javascripts/vue.min.js b/vendor/assets/javascripts/vue.min.js
new file mode 100644
index 00000000000..2c9a8a0e117
--- /dev/null
+++ b/vendor/assets/javascripts/vue.min.js
@@ -0,0 +1,9 @@
+/*!
+ * Vue.js v1.0.26
+ * (c) 2016 Evan You
+ * Released under the MIT License.
+ */
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.Vue=e()}(this,function(){"use strict";function t(e,n,r){if(i(e,n))return void(e[n]=r);if(e._isVue)return void t(e._data,n,r);var s=e.__ob__;if(!s)return void(e[n]=r);if(s.convert(n,r),s.dep.notify(),s.vms)for(var o=s.vms.length;o--;){var a=s.vms[o];a._proxy(n),a._digest()}return r}function e(t,e){if(i(t,e)){delete t[e];var n=t.__ob__;if(!n)return void(t._isVue&&(delete t._data[e],t._digest()));if(n.dep.notify(),n.vms)for(var r=n.vms.length;r--;){var s=n.vms[r];s._unproxy(e),s._digest()}}}function i(t,e){return Oi.call(t,e)}function n(t){return Ti.test(t)}function r(t){var e=(t+"").charCodeAt(0);return 36===e||95===e}function s(t){return null==t?"":t.toString()}function o(t){if("string"!=typeof t)return t;var e=Number(t);return isNaN(e)?t:e}function a(t){return"true"===t?!0:"false"===t?!1:t}function h(t){var e=t.charCodeAt(0),i=t.charCodeAt(t.length-1);return e!==i||34!==e&&39!==e?t:t.slice(1,-1)}function l(t){return t.replace(Ni,c)}function c(t,e){return e?e.toUpperCase():""}function u(t){return t.replace(ji,"$1-$2").toLowerCase()}function f(t){return t.replace(Ei,c)}function p(t,e){return function(i){var n=arguments.length;return n?n>1?t.apply(e,arguments):t.call(e,i):t.call(e)}}function d(t,e){e=e||0;for(var i=t.length-e,n=new Array(i);i--;)n[i]=t[i+e];return n}function v(t,e){for(var i=Object.keys(e),n=i.length;n--;)t[i[n]]=e[i[n]];return t}function m(t){return null!==t&&"object"==typeof t}function g(t){return Si.call(t)===Fi}function _(t,e,i,n){Object.defineProperty(t,e,{value:i,enumerable:!!n,writable:!0,configurable:!0})}function y(t,e){var i,n,r,s,o,a=function h(){var a=Date.now()-s;e>a&&a>=0?i=setTimeout(h,e-a):(i=null,o=t.apply(r,n),i||(r=n=null))};return function(){return r=this,n=arguments,s=Date.now(),i||(i=setTimeout(a,e)),o}}function b(t,e){for(var i=t.length;i--;)if(t[i]===e)return i;return-1}function w(t){var e=function i(){return i.cancelled?void 0:t.apply(this,arguments)};return e.cancel=function(){e.cancelled=!0},e}function C(t,e){return t==e||(m(t)&&m(e)?JSON.stringify(t)===JSON.stringify(e):!1)}function $(t){this.size=0,this.limit=t,this.head=this.tail=void 0,this._keymap=Object.create(null)}function k(){var t,e=en.slice(hn,on).trim();if(e){t={};var i=e.match(vn);t.name=i[0],i.length>1&&(t.args=i.slice(1).map(x))}t&&(nn.filters=nn.filters||[]).push(t),hn=on+1}function x(t){if(mn.test(t))return{value:o(t),dynamic:!1};var e=h(t),i=e===t;return{value:i?t:e,dynamic:i}}function A(t){var e=dn.get(t);if(e)return e;for(en=t,ln=cn=!1,un=fn=pn=0,hn=0,nn={},on=0,an=en.length;an>on;on++)if(sn=rn,rn=en.charCodeAt(on),ln)39===rn&&92!==sn&&(ln=!ln);else if(cn)34===rn&&92!==sn&&(cn=!cn);else if(124===rn&&124!==en.charCodeAt(on+1)&&124!==en.charCodeAt(on-1))null==nn.expression?(hn=on+1,nn.expression=en.slice(0,on).trim()):k();else switch(rn){case 34:cn=!0;break;case 39:ln=!0;break;case 40:pn++;break;case 41:pn--;break;case 91:fn++;break;case 93:fn--;break;case 123:un++;break;case 125:un--}return null==nn.expression?nn.expression=en.slice(0,on).trim():0!==hn&&k(),dn.put(t,nn),nn}function O(t){return t.replace(_n,"\\$&")}function T(){var t=O(An.delimiters[0]),e=O(An.delimiters[1]),i=O(An.unsafeDelimiters[0]),n=O(An.unsafeDelimiters[1]);bn=new RegExp(i+"((?:.|\\n)+?)"+n+"|"+t+"((?:.|\\n)+?)"+e,"g"),wn=new RegExp("^"+i+"((?:.|\\n)+?)"+n+"$"),yn=new $(1e3)}function N(t){yn||T();var e=yn.get(t);if(e)return e;if(!bn.test(t))return null;for(var i,n,r,s,o,a,h=[],l=bn.lastIndex=0;i=bn.exec(t);)n=i.index,n>l&&h.push({value:t.slice(l,n)}),r=wn.test(i[0]),s=r?i[1]:i[2],o=s.charCodeAt(0),a=42===o,s=a?s.slice(1):s,h.push({tag:!0,value:s.trim(),html:r,oneTime:a}),l=n+i[0].length;return l<t.length&&h.push({value:t.slice(l)}),yn.put(t,h),h}function j(t,e){return t.length>1?t.map(function(t){return E(t,e)}).join("+"):E(t[0],e,!0)}function E(t,e,i){return t.tag?t.oneTime&&e?'"'+e.$eval(t.value)+'"':S(t.value,i):'"'+t.value+'"'}function S(t,e){if(Cn.test(t)){var i=A(t);return i.filters?"this._applyFilters("+i.expression+",null,"+JSON.stringify(i.filters)+",false)":"("+t+")"}return e?t:"("+t+")"}function F(t,e,i,n){R(t,1,function(){e.appendChild(t)},i,n)}function D(t,e,i,n){R(t,1,function(){B(t,e)},i,n)}function P(t,e,i){R(t,-1,function(){z(t)},e,i)}function R(t,e,i,n,r){var s=t.__v_trans;if(!s||!s.hooks&&!qi||!n._isCompiled||n.$parent&&!n.$parent._isCompiled)return i(),void(r&&r());var o=e>0?"enter":"leave";s[o](i,r)}function L(t){if("string"==typeof t){t=document.querySelector(t)}return t}function H(t){if(!t)return!1;var e=t.ownerDocument.documentElement,i=t.parentNode;return e===t||e===i||!(!i||1!==i.nodeType||!e.contains(i))}function I(t,e){var i=t.getAttribute(e);return null!==i&&t.removeAttribute(e),i}function M(t,e){var i=I(t,":"+e);return null===i&&(i=I(t,"v-bind:"+e)),i}function V(t,e){return t.hasAttribute(e)||t.hasAttribute(":"+e)||t.hasAttribute("v-bind:"+e)}function B(t,e){e.parentNode.insertBefore(t,e)}function W(t,e){e.nextSibling?B(t,e.nextSibling):e.parentNode.appendChild(t)}function z(t){t.parentNode.removeChild(t)}function U(t,e){e.firstChild?B(t,e.firstChild):e.appendChild(t)}function J(t,e){var i=t.parentNode;i&&i.replaceChild(e,t)}function q(t,e,i,n){t.addEventListener(e,i,n)}function Q(t,e,i){t.removeEventListener(e,i)}function G(t){var e=t.className;return"object"==typeof e&&(e=e.baseVal||""),e}function Z(t,e){Mi&&!/svg$/.test(t.namespaceURI)?t.className=e:t.setAttribute("class",e)}function X(t,e){if(t.classList)t.classList.add(e);else{var i=" "+G(t)+" ";i.indexOf(" "+e+" ")<0&&Z(t,(i+e).trim())}}function Y(t,e){if(t.classList)t.classList.remove(e);else{for(var i=" "+G(t)+" ",n=" "+e+" ";i.indexOf(n)>=0;)i=i.replace(n," ");Z(t,i.trim())}t.className||t.removeAttribute("class")}function K(t,e){var i,n;if(it(t)&&at(t.content)&&(t=t.content),t.hasChildNodes())for(tt(t),n=e?document.createDocumentFragment():document.createElement("div");i=t.firstChild;)n.appendChild(i);return n}function tt(t){for(var e;e=t.firstChild,et(e);)t.removeChild(e);for(;e=t.lastChild,et(e);)t.removeChild(e)}function et(t){return t&&(3===t.nodeType&&!t.data.trim()||8===t.nodeType)}function it(t){return t.tagName&&"template"===t.tagName.toLowerCase()}function nt(t,e){var i=An.debug?document.createComment(t):document.createTextNode(e?" ":"");return i.__v_anchor=!0,i}function rt(t){if(t.hasAttributes())for(var e=t.attributes,i=0,n=e.length;n>i;i++){var r=e[i].name;if(Nn.test(r))return l(r.replace(Nn,""))}}function st(t,e,i){for(var n;t!==e;)n=t.nextSibling,i(t),t=n;i(e)}function ot(t,e,i,n,r){function s(){if(a++,o&&a>=h.length){for(var t=0;t<h.length;t++)n.appendChild(h[t]);r&&r()}}var o=!1,a=0,h=[];st(t,e,function(t){t===e&&(o=!0),h.push(t),P(t,i,s)})}function at(t){return t&&11===t.nodeType}function ht(t){if(t.outerHTML)return t.outerHTML;var e=document.createElement("div");return e.appendChild(t.cloneNode(!0)),e.innerHTML}function lt(t,e){var i=t.tagName.toLowerCase(),n=t.hasAttributes();if(jn.test(i)||En.test(i)){if(n)return ct(t,e)}else{if(gt(e,"components",i))return{id:i};var r=n&&ct(t,e);if(r)return r}}function ct(t,e){var i=t.getAttribute("is");if(null!=i){if(gt(e,"components",i))return t.removeAttribute("is"),{id:i}}else if(i=M(t,"is"),null!=i)return{id:i,dynamic:!0}}function ut(e,n){var r,s,o;for(r in n)s=e[r],o=n[r],i(e,r)?m(s)&&m(o)&&ut(s,o):t(e,r,o);return e}function ft(t,e){var i=Object.create(t||null);return e?v(i,vt(e)):i}function pt(t){if(t.components)for(var e,i=t.components=vt(t.components),n=Object.keys(i),r=0,s=n.length;s>r;r++){var o=n[r];jn.test(o)||En.test(o)||(e=i[o],g(e)&&(i[o]=wi.extend(e)))}}function dt(t){var e,i,n=t.props;if(Di(n))for(t.props={},e=n.length;e--;)i=n[e],"string"==typeof i?t.props[i]=null:i.name&&(t.props[i.name]=i);else if(g(n)){var r=Object.keys(n);for(e=r.length;e--;)i=n[r[e]],"function"==typeof i&&(n[r[e]]={type:i})}}function vt(t){if(Di(t)){for(var e,i={},n=t.length;n--;){e=t[n];var r="function"==typeof e?e.options&&e.options.name||e.id:e.name||e.id;r&&(i[r]=e)}return i}return t}function mt(t,e,n){function r(i){var r=Sn[i]||Fn;o[i]=r(t[i],e[i],n,i)}pt(e),dt(e);var s,o={};if(e["extends"]&&(t="function"==typeof e["extends"]?mt(t,e["extends"].options,n):mt(t,e["extends"],n)),e.mixins)for(var a=0,h=e.mixins.length;h>a;a++){var l=e.mixins[a],c=l.prototype instanceof wi?l.options:l;t=mt(t,c,n)}for(s in t)r(s);for(s in e)i(t,s)||r(s);return o}function gt(t,e,i,n){if("string"==typeof i){var r,s=t[e],o=s[i]||s[r=l(i)]||s[r.charAt(0).toUpperCase()+r.slice(1)];return o}}function _t(){this.id=Dn++,this.subs=[]}function yt(t){Hn=!1,t(),Hn=!0}function bt(t){if(this.value=t,this.dep=new _t,_(t,"__ob__",this),Di(t)){var e=Pi?wt:Ct;e(t,Rn,Ln),this.observeArray(t)}else this.walk(t)}function wt(t,e){t.__proto__=e}function Ct(t,e,i){for(var n=0,r=i.length;r>n;n++){var s=i[n];_(t,s,e[s])}}function $t(t,e){if(t&&"object"==typeof t){var n;return i(t,"__ob__")&&t.__ob__ instanceof bt?n=t.__ob__:Hn&&(Di(t)||g(t))&&Object.isExtensible(t)&&!t._isVue&&(n=new bt(t)),n&&e&&n.addVm(e),n}}function kt(t,e,i){var n=new _t,r=Object.getOwnPropertyDescriptor(t,e);if(!r||r.configurable!==!1){var s=r&&r.get,o=r&&r.set,a=$t(i);Object.defineProperty(t,e,{enumerable:!0,configurable:!0,get:function(){var e=s?s.call(t):i;if(_t.target&&(n.depend(),a&&a.dep.depend(),Di(e)))for(var r,o=0,h=e.length;h>o;o++)r=e[o],r&&r.__ob__&&r.__ob__.dep.depend();return e},set:function(e){var r=s?s.call(t):i;e!==r&&(o?o.call(t,e):i=e,a=$t(e),n.notify())}})}}function xt(t){t.prototype._init=function(t){t=t||{},this.$el=null,this.$parent=t.parent,this.$root=this.$parent?this.$parent.$root:this,this.$children=[],this.$refs={},this.$els={},this._watchers=[],this._directives=[],this._uid=Mn++,this._isVue=!0,this._events={},this._eventsCount={},this._isFragment=!1,this._fragment=this._fragmentStart=this._fragmentEnd=null,this._isCompiled=this._isDestroyed=this._isReady=this._isAttached=this._isBeingDestroyed=this._vForRemoving=!1,this._unlinkFn=null,this._context=t._context||this.$parent,this._scope=t._scope,this._frag=t._frag,this._frag&&this._frag.children.push(this),this.$parent&&this.$parent.$children.push(this),t=this.$options=mt(this.constructor.options,t,this),this._updateRef(),this._data={},this._callHook("init"),this._initState(),this._initEvents(),this._callHook("created"),t.el&&this.$mount(t.el)}}function At(t){if(void 0===t)return"eof";var e=t.charCodeAt(0);switch(e){case 91:case 93:case 46:case 34:case 39:case 48:return t;case 95:case 36:return"ident";case 32:case 9:case 10:case 13:case 160:case 65279:case 8232:case 8233:return"ws"}return e>=97&&122>=e||e>=65&&90>=e?"ident":e>=49&&57>=e?"number":"else"}function Ot(t){var e=t.trim();return"0"===t.charAt(0)&&isNaN(t)?!1:n(e)?h(e):"*"+e}function Tt(t){function e(){var e=t[c+1];return u===Xn&&"'"===e||u===Yn&&'"'===e?(c++,n="\\"+e,p[Bn](),!0):void 0}var i,n,r,s,o,a,h,l=[],c=-1,u=Jn,f=0,p=[];for(p[Wn]=function(){void 0!==r&&(l.push(r),r=void 0)},p[Bn]=function(){void 0===r?r=n:r+=n},p[zn]=function(){p[Bn](),f++},p[Un]=function(){if(f>0)f--,u=Zn,p[Bn]();else{if(f=0,r=Ot(r),r===!1)return!1;p[Wn]()}};null!=u;)if(c++,i=t[c],"\\"!==i||!e()){if(s=At(i),h=er[u],o=h[s]||h["else"]||tr,o===tr)return;if(u=o[0],a=p[o[1]],a&&(n=o[2],n=void 0===n?i:n,a()===!1))return;if(u===Kn)return l.raw=t,l}}function Nt(t){var e=Vn.get(t);return e||(e=Tt(t),e&&Vn.put(t,e)),e}function jt(t,e){return It(e).get(t)}function Et(e,i,n){var r=e;if("string"==typeof i&&(i=Tt(i)),!i||!m(e))return!1;for(var s,o,a=0,h=i.length;h>a;a++)s=e,o=i[a],"*"===o.charAt(0)&&(o=It(o.slice(1)).get.call(r,r)),h-1>a?(e=e[o],m(e)||(e={},t(s,o,e))):Di(e)?e.$set(o,n):o in e?e[o]=n:t(e,o,n);return!0}function St(){}function Ft(t,e){var i=vr.length;return vr[i]=e?t.replace(lr,"\\n"):t,'"'+i+'"'}function Dt(t){var e=t.charAt(0),i=t.slice(1);return sr.test(i)?t:(i=i.indexOf('"')>-1?i.replace(ur,Pt):i,e+"scope."+i)}function Pt(t,e){return vr[e]}function Rt(t){ar.test(t),vr.length=0;var e=t.replace(cr,Ft).replace(hr,"");return e=(" "+e).replace(pr,Dt).replace(ur,Pt),Lt(e)}function Lt(t){try{return new Function("scope","return "+t+";")}catch(e){return St}}function Ht(t){var e=Nt(t);return e?function(t,i){Et(t,e,i)}:void 0}function It(t,e){t=t.trim();var i=nr.get(t);if(i)return e&&!i.set&&(i.set=Ht(i.exp)),i;var n={exp:t};return n.get=Mt(t)&&t.indexOf("[")<0?Lt("scope."+t):Rt(t),e&&(n.set=Ht(t)),nr.put(t,n),n}function Mt(t){return fr.test(t)&&!dr.test(t)&&"Math."!==t.slice(0,5)}function Vt(){gr.length=0,_r.length=0,yr={},br={},wr=!1}function Bt(){for(var t=!0;t;)t=!1,Wt(gr),Wt(_r),gr.length?t=!0:(Li&&An.devtools&&Li.emit("flush"),Vt())}function Wt(t){for(var e=0;e<t.length;e++){var i=t[e],n=i.id;yr[n]=null,i.run()}t.length=0}function zt(t){var e=t.id;if(null==yr[e]){var i=t.user?_r:gr;yr[e]=i.length,i.push(t),wr||(wr=!0,Yi(Bt))}}function Ut(t,e,i,n){n&&v(this,n);var r="function"==typeof e;if(this.vm=t,t._watchers.push(this),this.expression=e,this.cb=i,this.id=++Cr,this.active=!0,this.dirty=this.lazy,this.deps=[],this.newDeps=[],this.depIds=new Ki,this.newDepIds=new Ki,this.prevError=null,r)this.getter=e,this.setter=void 0;else{var s=It(e,this.twoWay);this.getter=s.get,this.setter=s.set}this.value=this.lazy?void 0:this.get(),this.queued=this.shallow=!1}function Jt(t,e){var i=void 0,n=void 0;e||(e=$r,e.clear());var r=Di(t),s=m(t);if((r||s)&&Object.isExtensible(t)){if(t.__ob__){var o=t.__ob__.dep.id;if(e.has(o))return;e.add(o)}if(r)for(i=t.length;i--;)Jt(t[i],e);else if(s)for(n=Object.keys(t),i=n.length;i--;)Jt(t[n[i]],e)}}function qt(t){return it(t)&&at(t.content)}function Qt(t,e){var i=e?t:t.trim(),n=xr.get(i);if(n)return n;var r=document.createDocumentFragment(),s=t.match(Tr),o=Nr.test(t),a=jr.test(t);if(s||o||a){var h=s&&s[1],l=Or[h]||Or.efault,c=l[0],u=l[1],f=l[2],p=document.createElement("div");for(p.innerHTML=u+t+f;c--;)p=p.lastChild;for(var d;d=p.firstChild;)r.appendChild(d)}else r.appendChild(document.createTextNode(t));return e||tt(r),xr.put(i,r),r}function Gt(t){if(qt(t))return Qt(t.innerHTML);if("SCRIPT"===t.tagName)return Qt(t.textContent);for(var e,i=Zt(t),n=document.createDocumentFragment();e=i.firstChild;)n.appendChild(e);return tt(n),n}function Zt(t){if(!t.querySelectorAll)return t.cloneNode();var e,i,n,r=t.cloneNode(!0);if(Er){var s=r;if(qt(t)&&(t=t.content,s=r.content),i=t.querySelectorAll("template"),i.length)for(n=s.querySelectorAll("template"),e=n.length;e--;)n[e].parentNode.replaceChild(Zt(i[e]),n[e])}if(Sr)if("TEXTAREA"===t.tagName)r.value=t.value;else if(i=t.querySelectorAll("textarea"),i.length)for(n=r.querySelectorAll("textarea"),e=n.length;e--;)n[e].value=i[e].value;return r}function Xt(t,e,i){var n,r;return at(t)?(tt(t),e?Zt(t):t):("string"==typeof t?i||"#"!==t.charAt(0)?r=Qt(t,i):(r=Ar.get(t),r||(n=document.getElementById(t.slice(1)),n&&(r=Gt(n),Ar.put(t,r)))):t.nodeType&&(r=Gt(t)),r&&e?Zt(r):r)}function Yt(t,e,i,n,r,s){this.children=[],this.childFrags=[],this.vm=e,this.scope=r,this.inserted=!1,this.parentFrag=s,s&&s.childFrags.push(this),this.unlink=t(e,i,n,r,this);var o=this.single=1===i.childNodes.length&&!i.childNodes[0].__v_anchor;o?(this.node=i.childNodes[0],this.before=Kt,this.remove=te):(this.node=nt("fragment-start"),this.end=nt("fragment-end"),this.frag=i,U(this.node,i),i.appendChild(this.end),this.before=ee,this.remove=ie),this.node.__v_frag=this}function Kt(t,e){this.inserted=!0;var i=e!==!1?D:B;i(this.node,t,this.vm),H(this.node)&&this.callHook(ne)}function te(){this.inserted=!1;var t=H(this.node),e=this;this.beforeRemove(),P(this.node,this.vm,function(){t&&e.callHook(re),e.destroy()})}function ee(t,e){this.inserted=!0;var i=this.vm,n=e!==!1?D:B;st(this.node,this.end,function(e){n(e,t,i)}),H(this.node)&&this.callHook(ne)}function ie(){this.inserted=!1;var t=this,e=H(this.node);this.beforeRemove(),ot(this.node,this.end,this.vm,this.frag,function(){e&&t.callHook(re),t.destroy()})}function ne(t){!t._isAttached&&H(t.$el)&&t._callHook("attached")}function re(t){t._isAttached&&!H(t.$el)&&t._callHook("detached")}function se(t,e){this.vm=t;var i,n="string"==typeof e;n||it(e)&&!e.hasAttribute("v-if")?i=Xt(e,!0):(i=document.createDocumentFragment(),i.appendChild(e)),this.template=i;var r,s=t.constructor.cid;if(s>0){var o=s+(n?e:ht(e));r=Pr.get(o),r||(r=De(i,t.$options,!0),Pr.put(o,r))}else r=De(i,t.$options,!0);this.linker=r}function oe(t,e,i){var n=t.node.previousSibling;if(n){for(t=n.__v_frag;!(t&&t.forId===i&&t.inserted||n===e);){if(n=n.previousSibling,!n)return;t=n.__v_frag}return t}}function ae(t){var e=t.node;if(t.end)for(;!e.__vue__&&e!==t.end&&e.nextSibling;)e=e.nextSibling;return e.__vue__}function he(t){for(var e=-1,i=new Array(Math.floor(t));++e<t;)i[e]=e;return i}function le(t,e,i,n){return n?"$index"===n?t:n.charAt(0).match(/\w/)?jt(i,n):i[n]:e||i}function ce(t,e,i){for(var n,r,s,o=e?[]:null,a=0,h=t.options.length;h>a;a++)if(n=t.options[a],s=i?n.hasAttribute("selected"):n.selected){if(r=n.hasOwnProperty("_value")?n._value:n.value,!e)return r;o.push(r)}return o}function ue(t,e){for(var i=t.length;i--;)if(C(t[i],e))return i;return-1}function fe(t,e){var i=e.map(function(t){var e=t.charCodeAt(0);return e>47&&58>e?parseInt(t,10):1===t.length&&(e=t.toUpperCase().charCodeAt(0),e>64&&91>e)?e:is[t]});return i=[].concat.apply([],i),function(e){return i.indexOf(e.keyCode)>-1?t.call(this,e):void 0}}function pe(t){return function(e){return e.stopPropagation(),t.call(this,e)}}function de(t){return function(e){return e.preventDefault(),t.call(this,e)}}function ve(t){return function(e){return e.target===e.currentTarget?t.call(this,e):void 0}}function me(t){if(as[t])return as[t];var e=ge(t);return as[t]=as[e]=e,e}function ge(t){t=u(t);var e=l(t),i=e.charAt(0).toUpperCase()+e.slice(1);hs||(hs=document.createElement("div"));var n,r=rs.length;if("filter"!==e&&e in hs.style)return{kebab:t,camel:e};for(;r--;)if(n=ss[r]+i,n in hs.style)return{kebab:rs[r]+t,camel:n}}function _e(t){var e=[];if(Di(t))for(var i=0,n=t.length;n>i;i++){var r=t[i];if(r)if("string"==typeof r)e.push(r);else for(var s in r)r[s]&&e.push(s)}else if(m(t))for(var o in t)t[o]&&e.push(o);return e}function ye(t,e,i){if(e=e.trim(),-1===e.indexOf(" "))return void i(t,e);for(var n=e.split(/\s+/),r=0,s=n.length;s>r;r++)i(t,n[r])}function be(t,e,i){function n(){++s>=r?i():t[s].call(e,n)}var r=t.length,s=0;t[0].call(e,n)}function we(t,e,i){for(var r,s,o,a,h,c,f,p=[],d=Object.keys(e),v=d.length;v--;)s=d[v],r=e[s]||ks,h=l(s),xs.test(h)&&(f={name:s,path:h,options:r,mode:$s.ONE_WAY,raw:null},o=u(s),null===(a=M(t,o))&&(null!==(a=M(t,o+".sync"))?f.mode=$s.TWO_WAY:null!==(a=M(t,o+".once"))&&(f.mode=$s.ONE_TIME)),null!==a?(f.raw=a,c=A(a),a=c.expression,f.filters=c.filters,n(a)&&!c.filters?f.optimizedLiteral=!0:f.dynamic=!0,f.parentPath=a):null!==(a=I(t,o))&&(f.raw=a),p.push(f));return Ce(p)}function Ce(t){return function(e,n){e._props={};for(var r,s,l,c,f,p=e.$options.propsData,d=t.length;d--;)if(r=t[d],f=r.raw,s=r.path,l=r.options,e._props[s]=r,p&&i(p,s)&&ke(e,r,p[s]),null===f)ke(e,r,void 0);else if(r.dynamic)r.mode===$s.ONE_TIME?(c=(n||e._context||e).$get(r.parentPath),ke(e,r,c)):e._context?e._bindDir({name:"prop",def:Os,prop:r},null,null,n):ke(e,r,e.$get(r.parentPath));else if(r.optimizedLiteral){var v=h(f);c=v===f?a(o(f)):v,ke(e,r,c)}else c=l.type!==Boolean||""!==f&&f!==u(r.name)?f:!0,ke(e,r,c)}}function $e(t,e,i,n){var r=e.dynamic&&Mt(e.parentPath),s=i;void 0===s&&(s=Ae(t,e)),s=Te(e,s,t);var o=s!==i;Oe(e,s,t)||(s=void 0),r&&!o?yt(function(){n(s)}):n(s)}function ke(t,e,i){$e(t,e,i,function(i){kt(t,e.path,i)})}function xe(t,e,i){$e(t,e,i,function(i){t[e.path]=i})}function Ae(t,e){var n=e.options;if(!i(n,"default"))return n.type===Boolean?!1:void 0;var r=n["default"];return m(r),"function"==typeof r&&n.type!==Function?r.call(t):r}function Oe(t,e,i){if(!t.options.required&&(null===t.raw||null==e))return!0;var n=t.options,r=n.type,s=!r,o=[];if(r){Di(r)||(r=[r]);for(var a=0;a<r.length&&!s;a++){var h=Ne(e,r[a]);o.push(h.expectedType),s=h.valid}}if(!s)return!1;var l=n.validator;return!l||l(e)}function Te(t,e,i){var n=t.options.coerce;return n&&"function"==typeof n?n(e):e}function Ne(t,e){var i,n;return e===String?(n="string",i=typeof t===n):e===Number?(n="number",i=typeof t===n):e===Boolean?(n="boolean",i=typeof t===n):e===Function?(n="function",i=typeof t===n):e===Object?(n="object",i=g(t)):e===Array?(n="array",i=Di(t)):i=t instanceof e,{valid:i,expectedType:n}}function je(t){Ts.push(t),Ns||(Ns=!0,Yi(Ee))}function Ee(){for(var t=document.documentElement.offsetHeight,e=0;e<Ts.length;e++)Ts[e]();return Ts=[],Ns=!1,t}function Se(t,e,i,n){this.id=e,this.el=t,this.enterClass=i&&i.enterClass||e+"-enter",this.leaveClass=i&&i.leaveClass||e+"-leave",this.hooks=i,this.vm=n,this.pendingCssEvent=this.pendingCssCb=this.cancel=this.pendingJsCb=this.op=this.cb=null,this.justEntered=!1,this.entered=this.left=!1,this.typeCache={},this.type=i&&i.type;var r=this;["enterNextTick","enterDone","leaveNextTick","leaveDone"].forEach(function(t){r[t]=p(r[t],r)})}function Fe(t){if(/svg$/.test(t.namespaceURI)){var e=t.getBoundingClientRect();return!(e.width||e.height)}return!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)}function De(t,e,i){var n=i||!e._asComponent?Ve(t,e):null,r=n&&n.terminal||ri(t)||!t.hasChildNodes()?null:qe(t.childNodes,e);return function(t,e,i,s,o){var a=d(e.childNodes),h=Pe(function(){n&&n(t,e,i,s,o),r&&r(t,a,i,s,o)},t);return Le(t,h)}}function Pe(t,e){e._directives=[];var i=e._directives.length;t();var n=e._directives.slice(i);n.sort(Re);for(var r=0,s=n.length;s>r;r++)n[r]._bind();return n}function Re(t,e){return t=t.descriptor.def.priority||zs,e=e.descriptor.def.priority||zs,t>e?-1:t===e?0:1}function Le(t,e,i,n){function r(r){He(t,e,r),i&&n&&He(i,n)}return r.dirs=e,r}function He(t,e,i){for(var n=e.length;n--;)e[n]._teardown()}function Ie(t,e,i,n){var r=we(e,i,t),s=Pe(function(){r(t,n)},t);return Le(t,s)}function Me(t,e,i){var n,r,s=e._containerAttrs,o=e._replacerAttrs;return 11!==t.nodeType&&(e._asComponent?(s&&i&&(n=ti(s,i)),o&&(r=ti(o,e))):r=ti(t.attributes,e)),e._containerAttrs=e._replacerAttrs=null,function(t,e,i){var s,o=t._context;o&&n&&(s=Pe(function(){n(o,e,null,i)},o));var a=Pe(function(){r&&r(t,e)},t);return Le(t,a,o,s)}}function Ve(t,e){var i=t.nodeType;return 1!==i||ri(t)?3===i&&t.data.trim()?We(t,e):null:Be(t,e)}function Be(t,e){if("TEXTAREA"===t.tagName){var i=N(t.value);i&&(t.setAttribute(":value",j(i)),t.value="")}var n,r=t.hasAttributes(),s=r&&d(t.attributes);return r&&(n=Xe(t,s,e)),n||(n=Ge(t,e)),n||(n=Ze(t,e)),!n&&r&&(n=ti(s,e)),n}function We(t,e){if(t._skip)return ze;var i=N(t.wholeText);if(!i)return null;for(var n=t.nextSibling;n&&3===n.nodeType;)n._skip=!0,n=n.nextSibling;for(var r,s,o=document.createDocumentFragment(),a=0,h=i.length;h>a;a++)s=i[a],r=s.tag?Ue(s,e):document.createTextNode(s.value),o.appendChild(r);return Je(i,o,e)}function ze(t,e){z(e)}function Ue(t,e){function i(e){if(!t.descriptor){var i=A(t.value);t.descriptor={name:e,def:bs[e],expression:i.expression,filters:i.filters}}}var n;return t.oneTime?n=document.createTextNode(t.value):t.html?(n=document.createComment("v-html"),i("html")):(n=document.createTextNode(" "),i("text")),n}function Je(t,e){return function(i,n,r,o){for(var a,h,l,c=e.cloneNode(!0),u=d(c.childNodes),f=0,p=t.length;p>f;f++)a=t[f],h=a.value,a.tag&&(l=u[f],a.oneTime?(h=(o||i).$eval(h),a.html?J(l,Xt(h,!0)):l.data=s(h)):i._bindDir(a.descriptor,l,r,o));J(n,c)}}function qe(t,e){for(var i,n,r,s=[],o=0,a=t.length;a>o;o++)r=t[o],i=Ve(r,e),n=i&&i.terminal||"SCRIPT"===r.tagName||!r.hasChildNodes()?null:qe(r.childNodes,e),s.push(i,n);return s.length?Qe(s):null}function Qe(t){return function(e,i,n,r,s){for(var o,a,h,l=0,c=0,u=t.length;u>l;c++){o=i[c],a=t[l++],h=t[l++];var f=d(o.childNodes);a&&a(e,o,n,r,s),h&&h(e,f,n,r,s)}}}function Ge(t,e){var i=t.tagName.toLowerCase();if(!jn.test(i)){var n=gt(e,"elementDirectives",i);return n?Ke(t,i,"",e,n):void 0}}function Ze(t,e){var i=lt(t,e);if(i){var n=rt(t),r={name:"component",ref:n,expression:i.id,def:Hs.component,modifiers:{literal:!i.dynamic}},s=function(t,e,i,s,o){n&&kt((s||t).$refs,n,null),t._bindDir(r,e,i,s,o)};return s.terminal=!0,s}}function Xe(t,e,i){if(null!==I(t,"v-pre"))return Ye;if(t.hasAttribute("v-else")){var n=t.previousElementSibling;if(n&&n.hasAttribute("v-if"))return Ye}for(var r,s,o,a,h,l,c,u,f,p,d=0,v=e.length;v>d;d++)r=e[d],s=r.name.replace(Bs,""),(h=s.match(Vs))&&(f=gt(i,"directives",h[1]),f&&f.terminal&&(!p||(f.priority||Us)>p.priority)&&(p=f,c=r.name,a=ei(r.name),o=r.value,l=h[1],u=h[2]));return p?Ke(t,l,o,i,p,c,u,a):void 0}function Ye(){}function Ke(t,e,i,n,r,s,o,a){var h=A(i),l={name:e,arg:o,expression:h.expression,filters:h.filters,raw:i,attr:s,modifiers:a,def:r};"for"!==e&&"router-view"!==e||(l.ref=rt(t));var c=function(t,e,i,n,r){l.ref&&kt((n||t).$refs,l.ref,null),t._bindDir(l,e,i,n,r)};return c.terminal=!0,c}function ti(t,e){function i(t,e,i){var n=i&&ni(i),r=!n&&A(s);v.push({name:t,attr:o,raw:a,def:e,arg:l,modifiers:c,expression:r&&r.expression,filters:r&&r.filters,interp:i,hasOneTime:n})}for(var n,r,s,o,a,h,l,c,u,f,p,d=t.length,v=[];d--;)if(n=t[d],r=o=n.name,s=a=n.value,f=N(s),l=null,c=ei(r),r=r.replace(Bs,""),f)s=j(f),l=r,i("bind",bs.bind,f);else if(Ws.test(r))c.literal=!Is.test(r),i("transition",Hs.transition);else if(Ms.test(r))l=r.replace(Ms,""),i("on",bs.on);else if(Is.test(r))h=r.replace(Is,""),"style"===h||"class"===h?i(h,Hs[h]):(l=h,i("bind",bs.bind));else if(p=r.match(Vs)){if(h=p[1],l=p[2],"else"===h)continue;u=gt(e,"directives",h,!0),u&&i(h,u)}return v.length?ii(v):void 0}function ei(t){var e=Object.create(null),i=t.match(Bs);if(i)for(var n=i.length;n--;)e[i[n].slice(1)]=!0;return e}function ii(t){return function(e,i,n,r,s){for(var o=t.length;o--;)e._bindDir(t[o],i,n,r,s)}}function ni(t){for(var e=t.length;e--;)if(t[e].oneTime)return!0}function ri(t){return"SCRIPT"===t.tagName&&(!t.hasAttribute("type")||"text/javascript"===t.getAttribute("type"))}function si(t,e){return e&&(e._containerAttrs=ai(t)),it(t)&&(t=Xt(t)),e&&(e._asComponent&&!e.template&&(e.template="<slot></slot>"),e.template&&(e._content=K(t),t=oi(t,e))),at(t)&&(U(nt("v-start",!0),t),t.appendChild(nt("v-end",!0))),t}function oi(t,e){var i=e.template,n=Xt(i,!0);if(n){var r=n.firstChild,s=r.tagName&&r.tagName.toLowerCase();return e.replace?(t===document.body,n.childNodes.length>1||1!==r.nodeType||"component"===s||gt(e,"components",s)||V(r,"is")||gt(e,"elementDirectives",s)||r.hasAttribute("v-for")||r.hasAttribute("v-if")?n:(e._replacerAttrs=ai(r),hi(t,r),r)):(t.appendChild(n),t)}}function ai(t){return 1===t.nodeType&&t.hasAttributes()?d(t.attributes):void 0}function hi(t,e){for(var i,n,r=t.attributes,s=r.length;s--;)i=r[s].name,n=r[s].value,e.hasAttribute(i)||Js.test(i)?"class"===i&&!N(n)&&(n=n.trim())&&n.split(/\s+/).forEach(function(t){X(e,t)}):e.setAttribute(i,n)}function li(t,e){if(e){for(var i,n,r=t._slotContents=Object.create(null),s=0,o=e.children.length;o>s;s++)i=e.children[s],(n=i.getAttribute("slot"))&&(r[n]||(r[n]=[])).push(i);for(n in r)r[n]=ci(r[n],e);if(e.hasChildNodes()){var a=e.childNodes;if(1===a.length&&3===a[0].nodeType&&!a[0].data.trim())return;r["default"]=ci(e.childNodes,e)}}}function ci(t,e){var i=document.createDocumentFragment();t=d(t);for(var n=0,r=t.length;r>n;n++){var s=t[n];!it(s)||s.hasAttribute("v-if")||s.hasAttribute("v-for")||(e.removeChild(s),s=Xt(s,!0)),i.appendChild(s)}return i}function ui(t){function e(){}function n(t,e){var i=new Ut(e,t,null,{lazy:!0});return function(){return i.dirty&&i.evaluate(),_t.target&&i.depend(),i.value}}Object.defineProperty(t.prototype,"$data",{get:function(){return this._data},set:function(t){t!==this._data&&this._setData(t)}}),t.prototype._initState=function(){this._initProps(),this._initMeta(),this._initMethods(),this._initData(),this._initComputed()},t.prototype._initProps=function(){var t=this.$options,e=t.el,i=t.props;e=t.el=L(e),this._propsUnlinkFn=e&&1===e.nodeType&&i?Ie(this,e,i,this._scope):null},t.prototype._initData=function(){var t=this.$options.data,e=this._data=t?t():{};g(e)||(e={});var n,r,s=this._props,o=Object.keys(e);for(n=o.length;n--;)r=o[n],s&&i(s,r)||this._proxy(r);$t(e,this)},t.prototype._setData=function(t){t=t||{};var e=this._data;this._data=t;var n,r,s;for(n=Object.keys(e),s=n.length;s--;)r=n[s],r in t||this._unproxy(r);for(n=Object.keys(t),s=n.length;s--;)r=n[s],i(this,r)||this._proxy(r);e.__ob__.removeVm(this),$t(t,this),this._digest()},t.prototype._proxy=function(t){if(!r(t)){var e=this;Object.defineProperty(e,t,{configurable:!0,enumerable:!0,get:function(){return e._data[t]},set:function(i){e._data[t]=i}})}},t.prototype._unproxy=function(t){r(t)||delete this[t]},t.prototype._digest=function(){for(var t=0,e=this._watchers.length;e>t;t++)this._watchers[t].update(!0)},t.prototype._initComputed=function(){var t=this.$options.computed;if(t)for(var i in t){var r=t[i],s={enumerable:!0,configurable:!0};"function"==typeof r?(s.get=n(r,this),s.set=e):(s.get=r.get?r.cache!==!1?n(r.get,this):p(r.get,this):e,s.set=r.set?p(r.set,this):e),Object.defineProperty(this,i,s)}},t.prototype._initMethods=function(){var t=this.$options.methods;if(t)for(var e in t)this[e]=p(t[e],this)},t.prototype._initMeta=function(){var t=this.$options._meta;if(t)for(var e in t)kt(this,e,t[e])}}function fi(t){function e(t,e){for(var i,n,r,s=e.attributes,o=0,a=s.length;a>o;o++)i=s[o].name,Qs.test(i)&&(i=i.replace(Qs,""),n=s[o].value,Mt(n)&&(n+=".apply(this, $arguments)"),r=(t._scope||t._context).$eval(n,!0),r._fromParent=!0,t.$on(i.replace(Qs),r))}function i(t,e,i){if(i){var r,s,o,a;for(s in i)if(r=i[s],Di(r))for(o=0,a=r.length;a>o;o++)n(t,e,s,r[o]);else n(t,e,s,r)}}function n(t,e,i,r,s){var o=typeof r;if("function"===o)t[e](i,r,s);else if("string"===o){var a=t.$options.methods,h=a&&a[r];h&&t[e](i,h,s)}else r&&"object"===o&&n(t,e,i,r.handler,r)}function r(){this._isAttached||(this._isAttached=!0,this.$children.forEach(s))}function s(t){!t._isAttached&&H(t.$el)&&t._callHook("attached")}function o(){this._isAttached&&(this._isAttached=!1,this.$children.forEach(a))}function a(t){t._isAttached&&!H(t.$el)&&t._callHook("detached")}t.prototype._initEvents=function(){var t=this.$options;t._asComponent&&e(this,t.el),i(this,"$on",t.events),i(this,"$watch",t.watch)},t.prototype._initDOMHooks=function(){this.$on("hook:attached",r),this.$on("hook:detached",o)},t.prototype._callHook=function(t){this.$emit("pre-hook:"+t);var e=this.$options[t];if(e)for(var i=0,n=e.length;n>i;i++)e[i].call(this);this.$emit("hook:"+t)}}function pi(){}function di(t,e,i,n,r,s){this.vm=e,this.el=i,this.descriptor=t,this.name=t.name,this.expression=t.expression,this.arg=t.arg,this.modifiers=t.modifiers,this.filters=t.filters,this.literal=this.modifiers&&this.modifiers.literal,this._locked=!1,this._bound=!1,this._listeners=null,this._host=n,this._scope=r,this._frag=s}function vi(t){t.prototype._updateRef=function(t){var e=this.$options._ref;if(e){var i=(this._scope||this._context).$refs;t?i[e]===this&&(i[e]=null):i[e]=this}},t.prototype._compile=function(t){var e=this.$options,i=t;if(t=si(t,e),this._initElement(t),1!==t.nodeType||null===I(t,"v-pre")){var n=this._context&&this._context.$options,r=Me(t,e,n);li(this,e._content);var s,o=this.constructor;e._linkerCachable&&(s=o.linker,s||(s=o.linker=De(t,e)));var a=r(this,t,this._scope),h=s?s(this,t):De(t,e)(this,t);this._unlinkFn=function(){a(),h(!0)},e.replace&&J(i,t),this._isCompiled=!0,this._callHook("compiled")}},t.prototype._initElement=function(t){at(t)?(this._isFragment=!0,this.$el=this._fragmentStart=t.firstChild,this._fragmentEnd=t.lastChild,3===this._fragmentStart.nodeType&&(this._fragmentStart.data=this._fragmentEnd.data=""),this._fragment=t):this.$el=t,this.$el.__vue__=this,this._callHook("beforeCompile")},t.prototype._bindDir=function(t,e,i,n,r){this._directives.push(new di(t,this,e,i,n,r))},t.prototype._destroy=function(t,e){if(this._isBeingDestroyed)return void(e||this._cleanup());var i,n,r=this,s=function(){!i||n||e||r._cleanup()};t&&this.$el&&(n=!0,this.$remove(function(){
+n=!1,s()})),this._callHook("beforeDestroy"),this._isBeingDestroyed=!0;var o,a=this.$parent;for(a&&!a._isBeingDestroyed&&(a.$children.$remove(this),this._updateRef(!0)),o=this.$children.length;o--;)this.$children[o].$destroy();for(this._propsUnlinkFn&&this._propsUnlinkFn(),this._unlinkFn&&this._unlinkFn(),o=this._watchers.length;o--;)this._watchers[o].teardown();this.$el&&(this.$el.__vue__=null),i=!0,s()},t.prototype._cleanup=function(){this._isDestroyed||(this._frag&&this._frag.children.$remove(this),this._data&&this._data.__ob__&&this._data.__ob__.removeVm(this),this.$el=this.$parent=this.$root=this.$children=this._watchers=this._context=this._scope=this._directives=null,this._isDestroyed=!0,this._callHook("destroyed"),this.$off())}}function mi(t){t.prototype._applyFilters=function(t,e,i,n){var r,s,o,a,h,l,c,u,f;for(l=0,c=i.length;c>l;l++)if(r=i[n?c-l-1:l],s=gt(this.$options,"filters",r.name,!0),s&&(s=n?s.write:s.read||s,"function"==typeof s)){if(o=n?[t,e]:[t],h=n?2:1,r.args)for(u=0,f=r.args.length;f>u;u++)a=r.args[u],o[u+h]=a.dynamic?this.$get(a.value):a.value;t=s.apply(this,o)}return t},t.prototype._resolveComponent=function(e,i){var n;if(n="function"==typeof e?e:gt(this.$options,"components",e,!0))if(n.options)i(n);else if(n.resolved)i(n.resolved);else if(n.requested)n.pendingCallbacks.push(i);else{n.requested=!0;var r=n.pendingCallbacks=[i];n.call(this,function(e){g(e)&&(e=t.extend(e)),n.resolved=e;for(var i=0,s=r.length;s>i;i++)r[i](e)},function(t){})}}}function gi(t){function i(t){return JSON.parse(JSON.stringify(t))}t.prototype.$get=function(t,e){var i=It(t);if(i){if(e){var n=this;return function(){n.$arguments=d(arguments);var t=i.get.call(n,n);return n.$arguments=null,t}}try{return i.get.call(this,this)}catch(r){}}},t.prototype.$set=function(t,e){var i=It(t,!0);i&&i.set&&i.set.call(this,this,e)},t.prototype.$delete=function(t){e(this._data,t)},t.prototype.$watch=function(t,e,i){var n,r=this;"string"==typeof t&&(n=A(t),t=n.expression);var s=new Ut(r,t,e,{deep:i&&i.deep,sync:i&&i.sync,filters:n&&n.filters,user:!i||i.user!==!1});return i&&i.immediate&&e.call(r,s.value),function(){s.teardown()}},t.prototype.$eval=function(t,e){if(Gs.test(t)){var i=A(t),n=this.$get(i.expression,e);return i.filters?this._applyFilters(n,null,i.filters):n}return this.$get(t,e)},t.prototype.$interpolate=function(t){var e=N(t),i=this;return e?1===e.length?i.$eval(e[0].value)+"":e.map(function(t){return t.tag?i.$eval(t.value):t.value}).join(""):t},t.prototype.$log=function(t){var e=t?jt(this._data,t):this._data;if(e&&(e=i(e)),!t){var n;for(n in this.$options.computed)e[n]=i(this[n]);if(this._props)for(n in this._props)e[n]=i(this[n])}console.log(e)}}function _i(t){function e(t,e,n,r,s,o){e=i(e);var a=!H(e),h=r===!1||a?s:o,l=!a&&!t._isAttached&&!H(t.$el);return t._isFragment?(st(t._fragmentStart,t._fragmentEnd,function(i){h(i,e,t)}),n&&n()):h(t.$el,e,t,n),l&&t._callHook("attached"),t}function i(t){return"string"==typeof t?document.querySelector(t):t}function n(t,e,i,n){e.appendChild(t),n&&n()}function r(t,e,i,n){B(t,e),n&&n()}function s(t,e,i){z(t),i&&i()}t.prototype.$nextTick=function(t){Yi(t,this)},t.prototype.$appendTo=function(t,i,r){return e(this,t,i,r,n,F)},t.prototype.$prependTo=function(t,e,n){return t=i(t),t.hasChildNodes()?this.$before(t.firstChild,e,n):this.$appendTo(t,e,n),this},t.prototype.$before=function(t,i,n){return e(this,t,i,n,r,D)},t.prototype.$after=function(t,e,n){return t=i(t),t.nextSibling?this.$before(t.nextSibling,e,n):this.$appendTo(t.parentNode,e,n),this},t.prototype.$remove=function(t,e){if(!this.$el.parentNode)return t&&t();var i=this._isAttached&&H(this.$el);i||(e=!1);var n=this,r=function(){i&&n._callHook("detached"),t&&t()};if(this._isFragment)ot(this._fragmentStart,this._fragmentEnd,this,this._fragment,r);else{var o=e===!1?s:P;o(this.$el,this,r)}return this}}function yi(t){function e(t,e,n){var r=t.$parent;if(r&&n&&!i.test(e))for(;r;)r._eventsCount[e]=(r._eventsCount[e]||0)+n,r=r.$parent}t.prototype.$on=function(t,i){return(this._events[t]||(this._events[t]=[])).push(i),e(this,t,1),this},t.prototype.$once=function(t,e){function i(){n.$off(t,i),e.apply(this,arguments)}var n=this;return i.fn=e,this.$on(t,i),this},t.prototype.$off=function(t,i){var n;if(!arguments.length){if(this.$parent)for(t in this._events)n=this._events[t],n&&e(this,t,-n.length);return this._events={},this}if(n=this._events[t],!n)return this;if(1===arguments.length)return e(this,t,-n.length),this._events[t]=null,this;for(var r,s=n.length;s--;)if(r=n[s],r===i||r.fn===i){e(this,t,-1),n.splice(s,1);break}return this},t.prototype.$emit=function(t){var e="string"==typeof t;t=e?t:t.name;var i=this._events[t],n=e||!i;if(i){i=i.length>1?d(i):i;var r=e&&i.some(function(t){return t._fromParent});r&&(n=!1);for(var s=d(arguments,1),o=0,a=i.length;a>o;o++){var h=i[o],l=h.apply(this,s);l!==!0||r&&!h._fromParent||(n=!0)}}return n},t.prototype.$broadcast=function(t){var e="string"==typeof t;if(t=e?t:t.name,this._eventsCount[t]){var i=this.$children,n=d(arguments);e&&(n[0]={name:t,source:this});for(var r=0,s=i.length;s>r;r++){var o=i[r],a=o.$emit.apply(o,n);a&&o.$broadcast.apply(o,n)}return this}},t.prototype.$dispatch=function(t){var e=this.$emit.apply(this,arguments);if(e){var i=this.$parent,n=d(arguments);for(n[0]={name:t,source:this};i;)e=i.$emit.apply(i,n),i=e?i.$parent:null;return this}};var i=/^hook:/}function bi(t){function e(){this._isAttached=!0,this._isReady=!0,this._callHook("ready")}t.prototype.$mount=function(t){return this._isCompiled?void 0:(t=L(t),t||(t=document.createElement("div")),this._compile(t),this._initDOMHooks(),H(this.$el)?(this._callHook("attached"),e.call(this)):this.$once("hook:attached",e),this)},t.prototype.$destroy=function(t,e){this._destroy(t,e)},t.prototype.$compile=function(t,e,i,n){return De(t,this.$options,!0)(this,t,e,i,n)}}function wi(t){this._init(t)}function Ci(t,e,i){return i=i?parseInt(i,10):0,e=o(e),"number"==typeof e?t.slice(i,i+e):t}function $i(t,e,i){if(t=Ks(t),null==e)return t;if("function"==typeof e)return t.filter(e);e=(""+e).toLowerCase();for(var n,r,s,o,a="in"===i?3:2,h=Array.prototype.concat.apply([],d(arguments,a)),l=[],c=0,u=t.length;u>c;c++)if(n=t[c],s=n&&n.$value||n,o=h.length){for(;o--;)if(r=h[o],"$key"===r&&xi(n.$key,e)||xi(jt(s,r),e)){l.push(n);break}}else xi(n,e)&&l.push(n);return l}function ki(t){function e(t,e,i){var r=n[i];return r&&("$key"!==r&&(m(t)&&"$value"in t&&(t=t.$value),m(e)&&"$value"in e&&(e=e.$value)),t=m(t)?jt(t,r):t,e=m(e)?jt(e,r):e),t===e?0:t>e?s:-s}var i=null,n=void 0;t=Ks(t);var r=d(arguments,1),s=r[r.length-1];"number"==typeof s?(s=0>s?-1:1,r=r.length>1?r.slice(0,-1):r):s=1;var o=r[0];return o?("function"==typeof o?i=function(t,e){return o(t,e)*s}:(n=Array.prototype.concat.apply([],r),i=function(t,r,s){return s=s||0,s>=n.length-1?e(t,r,s):e(t,r,s)||i(t,r,s+1)}),t.slice().sort(i)):t}function xi(t,e){var i;if(g(t)){var n=Object.keys(t);for(i=n.length;i--;)if(xi(t[n[i]],e))return!0}else if(Di(t)){for(i=t.length;i--;)if(xi(t[i],e))return!0}else if(null!=t)return t.toString().toLowerCase().indexOf(e)>-1}function Ai(i){function n(t){return new Function("return function "+f(t)+" (options) { this._init(options) }")()}i.options={directives:bs,elementDirectives:Ys,filters:eo,transitions:{},components:{},partials:{},replace:!0},i.util=In,i.config=An,i.set=t,i["delete"]=e,i.nextTick=Yi,i.compiler=qs,i.FragmentFactory=se,i.internalDirectives=Hs,i.parsers={path:ir,text:$n,template:Fr,directive:gn,expression:mr},i.cid=0;var r=1;i.extend=function(t){t=t||{};var e=this,i=0===e.cid;if(i&&t._Ctor)return t._Ctor;var s=t.name||e.options.name,o=n(s||"VueComponent");return o.prototype=Object.create(e.prototype),o.prototype.constructor=o,o.cid=r++,o.options=mt(e.options,t),o["super"]=e,o.extend=e.extend,An._assetTypes.forEach(function(t){o[t]=e[t]}),s&&(o.options.components[s]=o),i&&(t._Ctor=o),o},i.use=function(t){if(!t.installed){var e=d(arguments,1);return e.unshift(this),"function"==typeof t.install?t.install.apply(t,e):t.apply(null,e),t.installed=!0,this}},i.mixin=function(t){i.options=mt(i.options,t)},An._assetTypes.forEach(function(t){i[t]=function(e,n){return n?("component"===t&&g(n)&&(n.name||(n.name=e),n=i.extend(n)),this.options[t+"s"][e]=n,n):this.options[t+"s"][e]}}),v(i.transition,Tn)}var Oi=Object.prototype.hasOwnProperty,Ti=/^\s?(true|false|-?[\d\.]+|'[^']*'|"[^"]*")\s?$/,Ni=/-(\w)/g,ji=/([a-z\d])([A-Z])/g,Ei=/(?:^|[-_\/])(\w)/g,Si=Object.prototype.toString,Fi="[object Object]",Di=Array.isArray,Pi="__proto__"in{},Ri="undefined"!=typeof window&&"[object Object]"!==Object.prototype.toString.call(window),Li=Ri&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__,Hi=Ri&&window.navigator.userAgent.toLowerCase(),Ii=Hi&&Hi.indexOf("trident")>0,Mi=Hi&&Hi.indexOf("msie 9.0")>0,Vi=Hi&&Hi.indexOf("android")>0,Bi=Hi&&/(iphone|ipad|ipod|ios)/i.test(Hi),Wi=Bi&&Hi.match(/os ([\d_]+)/),zi=Wi&&Wi[1].split("_"),Ui=zi&&Number(zi[0])>=9&&Number(zi[1])>=3&&!window.indexedDB,Ji=void 0,qi=void 0,Qi=void 0,Gi=void 0;if(Ri&&!Mi){var Zi=void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend,Xi=void 0===window.onanimationend&&void 0!==window.onwebkitanimationend;Ji=Zi?"WebkitTransition":"transition",qi=Zi?"webkitTransitionEnd":"transitionend",Qi=Xi?"WebkitAnimation":"animation",Gi=Xi?"webkitAnimationEnd":"animationend"}var Yi=function(){function t(){n=!1;var t=i.slice(0);i=[];for(var e=0;e<t.length;e++)t[e]()}var e,i=[],n=!1;if("undefined"==typeof MutationObserver||Ui){var r=Ri?window:"undefined"!=typeof global?global:{};e=r.setImmediate||setTimeout}else{var s=1,o=new MutationObserver(t),a=document.createTextNode(s);o.observe(a,{characterData:!0}),e=function(){s=(s+1)%2,a.data=s}}return function(r,s){var o=s?function(){r.call(s)}:r;i.push(o),n||(n=!0,e(t,0))}}(),Ki=void 0;"undefined"!=typeof Set&&Set.toString().match(/native code/)?Ki=Set:(Ki=function(){this.set=Object.create(null)},Ki.prototype.has=function(t){return void 0!==this.set[t]},Ki.prototype.add=function(t){this.set[t]=1},Ki.prototype.clear=function(){this.set=Object.create(null)});var tn=$.prototype;tn.put=function(t,e){var i,n=this.get(t,!0);return n||(this.size===this.limit&&(i=this.shift()),n={key:t},this._keymap[t]=n,this.tail?(this.tail.newer=n,n.older=this.tail):this.head=n,this.tail=n,this.size++),n.value=e,i},tn.shift=function(){var t=this.head;return t&&(this.head=this.head.newer,this.head.older=void 0,t.newer=t.older=void 0,this._keymap[t.key]=void 0,this.size--),t},tn.get=function(t,e){var i=this._keymap[t];if(void 0!==i)return i===this.tail?e?i:i.value:(i.newer&&(i===this.head&&(this.head=i.newer),i.newer.older=i.older),i.older&&(i.older.newer=i.newer),i.newer=void 0,i.older=this.tail,this.tail&&(this.tail.newer=i),this.tail=i,e?i:i.value)};var en,nn,rn,sn,on,an,hn,ln,cn,un,fn,pn,dn=new $(1e3),vn=/[^\s'"]+|'[^']*'|"[^"]*"/g,mn=/^in$|^-?\d+/,gn=Object.freeze({parseDirective:A}),_n=/[-.*+?^${}()|[\]\/\\]/g,yn=void 0,bn=void 0,wn=void 0,Cn=/[^|]\|[^|]/,$n=Object.freeze({compileRegex:T,parseText:N,tokensToExp:j}),kn=["{{","}}"],xn=["{{{","}}}"],An=Object.defineProperties({debug:!1,silent:!1,async:!0,warnExpressionErrors:!0,devtools:!1,_delimitersChanged:!0,_assetTypes:["component","directive","elementDirective","filter","transition","partial"],_propBindingModes:{ONE_WAY:0,TWO_WAY:1,ONE_TIME:2},_maxUpdateCount:100},{delimiters:{get:function(){return kn},set:function(t){kn=t,T()},configurable:!0,enumerable:!0},unsafeDelimiters:{get:function(){return xn},set:function(t){xn=t,T()},configurable:!0,enumerable:!0}}),On=void 0,Tn=Object.freeze({appendWithTransition:F,beforeWithTransition:D,removeWithTransition:P,applyTransition:R}),Nn=/^v-ref:/,jn=/^(div|p|span|img|a|b|i|br|ul|ol|li|h1|h2|h3|h4|h5|h6|code|pre|table|th|td|tr|form|label|input|select|option|nav|article|section|header|footer)$/i,En=/^(slot|partial|component)$/i,Sn=An.optionMergeStrategies=Object.create(null);Sn.data=function(t,e,i){return i?t||e?function(){var n="function"==typeof e?e.call(i):e,r="function"==typeof t?t.call(i):void 0;return n?ut(n,r):r}:void 0:e?"function"!=typeof e?t:t?function(){return ut(e.call(this),t.call(this))}:e:t},Sn.el=function(t,e,i){if(i||!e||"function"==typeof e){var n=e||t;return i&&"function"==typeof n?n.call(i):n}},Sn.init=Sn.created=Sn.ready=Sn.attached=Sn.detached=Sn.beforeCompile=Sn.compiled=Sn.beforeDestroy=Sn.destroyed=Sn.activate=function(t,e){return e?t?t.concat(e):Di(e)?e:[e]:t},An._assetTypes.forEach(function(t){Sn[t+"s"]=ft}),Sn.watch=Sn.events=function(t,e){if(!e)return t;if(!t)return e;var i={};v(i,t);for(var n in e){var r=i[n],s=e[n];r&&!Di(r)&&(r=[r]),i[n]=r?r.concat(s):[s]}return i},Sn.props=Sn.methods=Sn.computed=function(t,e){if(!e)return t;if(!t)return e;var i=Object.create(null);return v(i,t),v(i,e),i};var Fn=function(t,e){return void 0===e?t:e},Dn=0;_t.target=null,_t.prototype.addSub=function(t){this.subs.push(t)},_t.prototype.removeSub=function(t){this.subs.$remove(t)},_t.prototype.depend=function(){_t.target.addDep(this)},_t.prototype.notify=function(){for(var t=d(this.subs),e=0,i=t.length;i>e;e++)t[e].update()};var Pn=Array.prototype,Rn=Object.create(Pn);["push","pop","shift","unshift","splice","sort","reverse"].forEach(function(t){var e=Pn[t];_(Rn,t,function(){for(var i=arguments.length,n=new Array(i);i--;)n[i]=arguments[i];var r,s=e.apply(this,n),o=this.__ob__;switch(t){case"push":r=n;break;case"unshift":r=n;break;case"splice":r=n.slice(2)}return r&&o.observeArray(r),o.dep.notify(),s})}),_(Pn,"$set",function(t,e){return t>=this.length&&(this.length=Number(t)+1),this.splice(t,1,e)[0]}),_(Pn,"$remove",function(t){if(this.length){var e=b(this,t);return e>-1?this.splice(e,1):void 0}});var Ln=Object.getOwnPropertyNames(Rn),Hn=!0;bt.prototype.walk=function(t){for(var e=Object.keys(t),i=0,n=e.length;n>i;i++)this.convert(e[i],t[e[i]])},bt.prototype.observeArray=function(t){for(var e=0,i=t.length;i>e;e++)$t(t[e])},bt.prototype.convert=function(t,e){kt(this.value,t,e)},bt.prototype.addVm=function(t){(this.vms||(this.vms=[])).push(t)},bt.prototype.removeVm=function(t){this.vms.$remove(t)};var In=Object.freeze({defineReactive:kt,set:t,del:e,hasOwn:i,isLiteral:n,isReserved:r,_toString:s,toNumber:o,toBoolean:a,stripQuotes:h,camelize:l,hyphenate:u,classify:f,bind:p,toArray:d,extend:v,isObject:m,isPlainObject:g,def:_,debounce:y,indexOf:b,cancellable:w,looseEqual:C,isArray:Di,hasProto:Pi,inBrowser:Ri,devtools:Li,isIE:Ii,isIE9:Mi,isAndroid:Vi,isIos:Bi,iosVersionMatch:Wi,iosVersion:zi,hasMutationObserverBug:Ui,get transitionProp(){return Ji},get transitionEndEvent(){return qi},get animationProp(){return Qi},get animationEndEvent(){return Gi},nextTick:Yi,get _Set(){return Ki},query:L,inDoc:H,getAttr:I,getBindAttr:M,hasBindAttr:V,before:B,after:W,remove:z,prepend:U,replace:J,on:q,off:Q,setClass:Z,addClass:X,removeClass:Y,extractContent:K,trimNode:tt,isTemplate:it,createAnchor:nt,findRef:rt,mapNodeRange:st,removeNodeRange:ot,isFragment:at,getOuterHTML:ht,mergeOptions:mt,resolveAsset:gt,checkComponentAttr:lt,commonTagRE:jn,reservedTagRE:En,warn:On}),Mn=0,Vn=new $(1e3),Bn=0,Wn=1,zn=2,Un=3,Jn=0,qn=1,Qn=2,Gn=3,Zn=4,Xn=5,Yn=6,Kn=7,tr=8,er=[];er[Jn]={ws:[Jn],ident:[Gn,Bn],"[":[Zn],eof:[Kn]},er[qn]={ws:[qn],".":[Qn],"[":[Zn],eof:[Kn]},er[Qn]={ws:[Qn],ident:[Gn,Bn]},er[Gn]={ident:[Gn,Bn],0:[Gn,Bn],number:[Gn,Bn],ws:[qn,Wn],".":[Qn,Wn],"[":[Zn,Wn],eof:[Kn,Wn]},er[Zn]={"'":[Xn,Bn],'"':[Yn,Bn],"[":[Zn,zn],"]":[qn,Un],eof:tr,"else":[Zn,Bn]},er[Xn]={"'":[Zn,Bn],eof:tr,"else":[Xn,Bn]},er[Yn]={'"':[Zn,Bn],eof:tr,"else":[Yn,Bn]};var ir=Object.freeze({parsePath:Nt,getPath:jt,setPath:Et}),nr=new $(1e3),rr="Math,Date,this,true,false,null,undefined,Infinity,NaN,isNaN,isFinite,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,parseInt,parseFloat",sr=new RegExp("^("+rr.replace(/,/g,"\\b|")+"\\b)"),or="break,case,class,catch,const,continue,debugger,default,delete,do,else,export,extends,finally,for,function,if,import,in,instanceof,let,return,super,switch,throw,try,var,while,with,yield,enum,await,implements,package,protected,static,interface,private,public",ar=new RegExp("^("+or.replace(/,/g,"\\b|")+"\\b)"),hr=/\s/g,lr=/\n/g,cr=/[\{,]\s*[\w\$_]+\s*:|('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`)|new |typeof |void /g,ur=/"(\d+)"/g,fr=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*$/,pr=/[^\w$\.](?:[A-Za-z_$][\w$]*)/g,dr=/^(?:true|false|null|undefined|Infinity|NaN)$/,vr=[],mr=Object.freeze({parseExpression:It,isSimplePath:Mt}),gr=[],_r=[],yr={},br={},wr=!1,Cr=0;Ut.prototype.get=function(){this.beforeGet();var t,e=this.scope||this.vm;try{t=this.getter.call(e,e)}catch(i){}return this.deep&&Jt(t),this.preProcess&&(t=this.preProcess(t)),this.filters&&(t=e._applyFilters(t,null,this.filters,!1)),this.postProcess&&(t=this.postProcess(t)),this.afterGet(),t},Ut.prototype.set=function(t){var e=this.scope||this.vm;this.filters&&(t=e._applyFilters(t,this.value,this.filters,!0));try{this.setter.call(e,e,t)}catch(i){}var n=e.$forContext;if(n&&n.alias===this.expression){if(n.filters)return;n._withLock(function(){e.$key?n.rawValue[e.$key]=t:n.rawValue.$set(e.$index,t)})}},Ut.prototype.beforeGet=function(){_t.target=this},Ut.prototype.addDep=function(t){var e=t.id;this.newDepIds.has(e)||(this.newDepIds.add(e),this.newDeps.push(t),this.depIds.has(e)||t.addSub(this))},Ut.prototype.afterGet=function(){_t.target=null;for(var t=this.deps.length;t--;){var e=this.deps[t];this.newDepIds.has(e.id)||e.removeSub(this)}var i=this.depIds;this.depIds=this.newDepIds,this.newDepIds=i,this.newDepIds.clear(),i=this.deps,this.deps=this.newDeps,this.newDeps=i,this.newDeps.length=0},Ut.prototype.update=function(t){this.lazy?this.dirty=!0:this.sync||!An.async?this.run():(this.shallow=this.queued?t?this.shallow:!1:!!t,this.queued=!0,zt(this))},Ut.prototype.run=function(){if(this.active){var t=this.get();if(t!==this.value||(m(t)||this.deep)&&!this.shallow){var e=this.value;this.value=t;this.prevError;this.cb.call(this.vm,t,e)}this.queued=this.shallow=!1}},Ut.prototype.evaluate=function(){var t=_t.target;this.value=this.get(),this.dirty=!1,_t.target=t},Ut.prototype.depend=function(){for(var t=this.deps.length;t--;)this.deps[t].depend()},Ut.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||this.vm._vForRemoving||this.vm._watchers.$remove(this);for(var t=this.deps.length;t--;)this.deps[t].removeSub(this);this.active=!1,this.vm=this.cb=this.value=null}};var $r=new Ki,kr={bind:function(){this.attr=3===this.el.nodeType?"data":"textContent"},update:function(t){this.el[this.attr]=s(t)}},xr=new $(1e3),Ar=new $(1e3),Or={efault:[0,"",""],legend:[1,"<fieldset>","</fieldset>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]};Or.td=Or.th=[3,"<table><tbody><tr>","</tr></tbody></table>"],Or.option=Or.optgroup=[1,'<select multiple="multiple">',"</select>"],Or.thead=Or.tbody=Or.colgroup=Or.caption=Or.tfoot=[1,"<table>","</table>"],Or.g=Or.defs=Or.symbol=Or.use=Or.image=Or.text=Or.circle=Or.ellipse=Or.line=Or.path=Or.polygon=Or.polyline=Or.rect=[1,'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events"version="1.1">',"</svg>"];var Tr=/<([\w:-]+)/,Nr=/&#?\w+?;/,jr=/<!--/,Er=function(){if(Ri){var t=document.createElement("div");return t.innerHTML="<template>1</template>",!t.cloneNode(!0).firstChild.innerHTML}return!1}(),Sr=function(){if(Ri){var t=document.createElement("textarea");return t.placeholder="t","t"===t.cloneNode(!0).value}return!1}(),Fr=Object.freeze({cloneNode:Zt,parseTemplate:Xt}),Dr={bind:function(){8===this.el.nodeType&&(this.nodes=[],this.anchor=nt("v-html"),J(this.el,this.anchor))},update:function(t){t=s(t),this.nodes?this.swap(t):this.el.innerHTML=t},swap:function(t){for(var e=this.nodes.length;e--;)z(this.nodes[e]);var i=Xt(t,!0,!0);this.nodes=d(i.childNodes),B(i,this.anchor)}};Yt.prototype.callHook=function(t){var e,i;for(e=0,i=this.childFrags.length;i>e;e++)this.childFrags[e].callHook(t);for(e=0,i=this.children.length;i>e;e++)t(this.children[e])},Yt.prototype.beforeRemove=function(){var t,e;for(t=0,e=this.childFrags.length;e>t;t++)this.childFrags[t].beforeRemove(!1);for(t=0,e=this.children.length;e>t;t++)this.children[t].$destroy(!1,!0);var i=this.unlink.dirs;for(t=0,e=i.length;e>t;t++)i[t]._watcher&&i[t]._watcher.teardown()},Yt.prototype.destroy=function(){this.parentFrag&&this.parentFrag.childFrags.$remove(this),this.node.__v_frag=null,this.unlink()};var Pr=new $(5e3);se.prototype.create=function(t,e,i){var n=Zt(this.template);return new Yt(this.linker,this.vm,n,t,e,i)};var Rr=700,Lr=800,Hr=850,Ir=1100,Mr=1500,Vr=1500,Br=1750,Wr=2100,zr=2200,Ur=2300,Jr=0,qr={priority:zr,terminal:!0,params:["track-by","stagger","enter-stagger","leave-stagger"],bind:function(){var t=this.expression.match(/(.*) (?:in|of) (.*)/);if(t){var e=t[1].match(/\((.*),(.*)\)/);e?(this.iterator=e[1].trim(),this.alias=e[2].trim()):this.alias=t[1].trim(),this.expression=t[2]}if(this.alias){this.id="__v-for__"+ ++Jr;var i=this.el.tagName;this.isOption=("OPTION"===i||"OPTGROUP"===i)&&"SELECT"===this.el.parentNode.tagName,this.start=nt("v-for-start"),this.end=nt("v-for-end"),J(this.el,this.end),B(this.start,this.end),this.cache=Object.create(null),this.factory=new se(this.vm,this.el)}},update:function(t){this.diff(t),this.updateRef(),this.updateModel()},diff:function(t){var e,n,r,s,o,a,h=t[0],l=this.fromObject=m(h)&&i(h,"$key")&&i(h,"$value"),c=this.params.trackBy,u=this.frags,f=this.frags=new Array(t.length),p=this.alias,d=this.iterator,v=this.start,g=this.end,_=H(v),y=!u;for(e=0,n=t.length;n>e;e++)h=t[e],s=l?h.$key:null,o=l?h.$value:h,a=!m(o),r=!y&&this.getCachedFrag(o,e,s),r?(r.reused=!0,r.scope.$index=e,s&&(r.scope.$key=s),d&&(r.scope[d]=null!==s?s:e),(c||l||a)&&yt(function(){r.scope[p]=o})):(r=this.create(o,p,e,s),r.fresh=!y),f[e]=r,y&&r.before(g);if(!y){var b=0,w=u.length-f.length;for(this.vm._vForRemoving=!0,e=0,n=u.length;n>e;e++)r=u[e],r.reused||(this.deleteCachedFrag(r),this.remove(r,b++,w,_));this.vm._vForRemoving=!1,b&&(this.vm._watchers=this.vm._watchers.filter(function(t){return t.active}));var C,$,k,x=0;for(e=0,n=f.length;n>e;e++)r=f[e],C=f[e-1],$=C?C.staggerCb?C.staggerAnchor:C.end||C.node:v,r.reused&&!r.staggerCb?(k=oe(r,v,this.id),k===C||k&&oe(k,v,this.id)===C||this.move(r,$)):this.insert(r,x++,$,_),r.reused=r.fresh=!1}},create:function(t,e,i,n){var r=this._host,s=this._scope||this.vm,o=Object.create(s);o.$refs=Object.create(s.$refs),o.$els=Object.create(s.$els),o.$parent=s,o.$forContext=this,yt(function(){kt(o,e,t)}),kt(o,"$index",i),n?kt(o,"$key",n):o.$key&&_(o,"$key",null),this.iterator&&kt(o,this.iterator,null!==n?n:i);var a=this.factory.create(r,o,this._frag);return a.forId=this.id,this.cacheFrag(t,a,i,n),a},updateRef:function(){var t=this.descriptor.ref;if(t){var e,i=(this._scope||this.vm).$refs;this.fromObject?(e={},this.frags.forEach(function(t){e[t.scope.$key]=ae(t)})):e=this.frags.map(ae),i[t]=e}},updateModel:function(){if(this.isOption){var t=this.start.parentNode,e=t&&t.__v_model;e&&e.forceUpdate()}},insert:function(t,e,i,n){t.staggerCb&&(t.staggerCb.cancel(),t.staggerCb=null);var r=this.getStagger(t,e,null,"enter");if(n&&r){var s=t.staggerAnchor;s||(s=t.staggerAnchor=nt("stagger-anchor"),s.__v_frag=t),W(s,i);var o=t.staggerCb=w(function(){t.staggerCb=null,t.before(s),z(s)});setTimeout(o,r)}else{var a=i.nextSibling;a||(W(this.end,i),a=this.end),t.before(a)}},remove:function(t,e,i,n){if(t.staggerCb)return t.staggerCb.cancel(),void(t.staggerCb=null);var r=this.getStagger(t,e,i,"leave");if(n&&r){var s=t.staggerCb=w(function(){t.staggerCb=null,t.remove()});setTimeout(s,r)}else t.remove()},move:function(t,e){e.nextSibling||this.end.parentNode.appendChild(this.end),t.before(e.nextSibling,!1)},cacheFrag:function(t,e,n,r){var s,o=this.params.trackBy,a=this.cache,h=!m(t);r||o||h?(s=le(n,r,t,o),a[s]||(a[s]=e)):(s=this.id,i(t,s)?null===t[s]&&(t[s]=e):Object.isExtensible(t)&&_(t,s,e)),e.raw=t},getCachedFrag:function(t,e,i){var n,r=this.params.trackBy,s=!m(t);if(i||r||s){var o=le(e,i,t,r);n=this.cache[o]}else n=t[this.id];return n&&(n.reused||n.fresh),n},deleteCachedFrag:function(t){var e=t.raw,n=this.params.trackBy,r=t.scope,s=r.$index,o=i(r,"$key")&&r.$key,a=!m(e);if(n||o||a){var h=le(s,o,e,n);this.cache[h]=null}else e[this.id]=null,t.raw=null},getStagger:function(t,e,i,n){n+="Stagger";var r=t.node.__v_trans,s=r&&r.hooks,o=s&&(s[n]||s.stagger);return o?o.call(t,e,i):e*parseInt(this.params[n]||this.params.stagger,10)},_preProcess:function(t){return this.rawValue=t,t},_postProcess:function(t){if(Di(t))return t;if(g(t)){for(var e,i=Object.keys(t),n=i.length,r=new Array(n);n--;)e=i[n],r[n]={$key:e,$value:t[e]};return r}return"number"!=typeof t||isNaN(t)||(t=he(t)),t||[]},unbind:function(){if(this.descriptor.ref&&((this._scope||this.vm).$refs[this.descriptor.ref]=null),this.frags)for(var t,e=this.frags.length;e--;)t=this.frags[e],this.deleteCachedFrag(t),t.destroy()}},Qr={priority:Wr,terminal:!0,bind:function(){var t=this.el;if(t.__vue__)this.invalid=!0;else{var e=t.nextElementSibling;e&&null!==I(e,"v-else")&&(z(e),this.elseEl=e),this.anchor=nt("v-if"),J(t,this.anchor)}},update:function(t){this.invalid||(t?this.frag||this.insert():this.remove())},insert:function(){this.elseFrag&&(this.elseFrag.remove(),this.elseFrag=null),this.factory||(this.factory=new se(this.vm,this.el)),this.frag=this.factory.create(this._host,this._scope,this._frag),this.frag.before(this.anchor)},remove:function(){this.frag&&(this.frag.remove(),this.frag=null),this.elseEl&&!this.elseFrag&&(this.elseFactory||(this.elseFactory=new se(this.elseEl._context||this.vm,this.elseEl)),this.elseFrag=this.elseFactory.create(this._host,this._scope,this._frag),this.elseFrag.before(this.anchor))},unbind:function(){this.frag&&this.frag.destroy(),this.elseFrag&&this.elseFrag.destroy()}},Gr={bind:function(){var t=this.el.nextElementSibling;t&&null!==I(t,"v-else")&&(this.elseEl=t)},update:function(t){this.apply(this.el,t),this.elseEl&&this.apply(this.elseEl,!t)},apply:function(t,e){function i(){t.style.display=e?"":"none"}H(t)?R(t,e?1:-1,i,this.vm):i()}},Zr={bind:function(){var t=this,e=this.el,i="range"===e.type,n=this.params.lazy,r=this.params.number,s=this.params.debounce,a=!1;if(Vi||i||(this.on("compositionstart",function(){a=!0}),this.on("compositionend",function(){a=!1,n||t.listener()})),this.focused=!1,i||n||(this.on("focus",function(){t.focused=!0}),this.on("blur",function(){t.focused=!1,t._frag&&!t._frag.inserted||t.rawListener()})),this.listener=this.rawListener=function(){if(!a&&t._bound){var n=r||i?o(e.value):e.value;t.set(n),Yi(function(){t._bound&&!t.focused&&t.update(t._watcher.value)})}},s&&(this.listener=y(this.listener,s)),this.hasjQuery="function"==typeof jQuery,this.hasjQuery){var h=jQuery.fn.on?"on":"bind";jQuery(e)[h]("change",this.rawListener),n||jQuery(e)[h]("input",this.listener)}else this.on("change",this.rawListener),n||this.on("input",this.listener);!n&&Mi&&(this.on("cut",function(){Yi(t.listener)}),this.on("keyup",function(e){46!==e.keyCode&&8!==e.keyCode||t.listener()})),(e.hasAttribute("value")||"TEXTAREA"===e.tagName&&e.value.trim())&&(this.afterBind=this.listener)},update:function(t){t=s(t),t!==this.el.value&&(this.el.value=t)},unbind:function(){var t=this.el;if(this.hasjQuery){var e=jQuery.fn.off?"off":"unbind";jQuery(t)[e]("change",this.listener),jQuery(t)[e]("input",this.listener)}}},Xr={bind:function(){var t=this,e=this.el;this.getValue=function(){if(e.hasOwnProperty("_value"))return e._value;var i=e.value;return t.params.number&&(i=o(i)),i},this.listener=function(){t.set(t.getValue())},this.on("change",this.listener),e.hasAttribute("checked")&&(this.afterBind=this.listener)},update:function(t){this.el.checked=C(t,this.getValue())}},Yr={bind:function(){var t=this,e=this,i=this.el;this.forceUpdate=function(){e._watcher&&e.update(e._watcher.get())};var n=this.multiple=i.hasAttribute("multiple");this.listener=function(){var t=ce(i,n);t=e.params.number?Di(t)?t.map(o):o(t):t,e.set(t)},this.on("change",this.listener);var r=ce(i,n,!0);(n&&r.length||!n&&null!==r)&&(this.afterBind=this.listener),this.vm.$on("hook:attached",function(){Yi(t.forceUpdate)}),H(i)||Yi(this.forceUpdate)},update:function(t){var e=this.el;e.selectedIndex=-1;for(var i,n,r=this.multiple&&Di(t),s=e.options,o=s.length;o--;)i=s[o],n=i.hasOwnProperty("_value")?i._value:i.value,i.selected=r?ue(t,n)>-1:C(t,n)},unbind:function(){this.vm.$off("hook:attached",this.forceUpdate)}},Kr={bind:function(){function t(){var t=i.checked;return t&&i.hasOwnProperty("_trueValue")?i._trueValue:!t&&i.hasOwnProperty("_falseValue")?i._falseValue:t}var e=this,i=this.el;this.getValue=function(){return i.hasOwnProperty("_value")?i._value:e.params.number?o(i.value):i.value},this.listener=function(){var n=e._watcher.value;if(Di(n)){var r=e.getValue();i.checked?b(n,r)<0&&n.push(r):n.$remove(r)}else e.set(t())},this.on("change",this.listener),i.hasAttribute("checked")&&(this.afterBind=this.listener)},update:function(t){var e=this.el;Di(t)?e.checked=b(t,this.getValue())>-1:e.hasOwnProperty("_trueValue")?e.checked=C(t,e._trueValue):e.checked=!!t}},ts={text:Zr,radio:Xr,select:Yr,checkbox:Kr},es={priority:Lr,twoWay:!0,handlers:ts,params:["lazy","number","debounce"],bind:function(){this.checkFilters(),this.hasRead&&!this.hasWrite;var t,e=this.el,i=e.tagName;if("INPUT"===i)t=ts[e.type]||ts.text;else if("SELECT"===i)t=ts.select;else{if("TEXTAREA"!==i)return;t=ts.text}e.__v_model=this,t.bind.call(this),this.update=t.update,this._unbind=t.unbind},checkFilters:function(){var t=this.filters;if(t)for(var e=t.length;e--;){var i=gt(this.vm.$options,"filters",t[e].name);("function"==typeof i||i.read)&&(this.hasRead=!0),i.write&&(this.hasWrite=!0)}},unbind:function(){this.el.__v_model=null,this._unbind&&this._unbind()}},is={esc:27,tab:9,enter:13,space:32,"delete":[8,46],up:38,left:37,right:39,down:40},ns={priority:Rr,acceptStatement:!0,keyCodes:is,bind:function(){if("IFRAME"===this.el.tagName&&"load"!==this.arg){var t=this;this.iframeBind=function(){q(t.el.contentWindow,t.arg,t.handler,t.modifiers.capture)},this.on("load",this.iframeBind)}},update:function(t){if(this.descriptor.raw||(t=function(){}),"function"==typeof t){this.modifiers.stop&&(t=pe(t)),this.modifiers.prevent&&(t=de(t)),this.modifiers.self&&(t=ve(t));var e=Object.keys(this.modifiers).filter(function(t){return"stop"!==t&&"prevent"!==t&&"self"!==t&&"capture"!==t});e.length&&(t=fe(t,e)),this.reset(),this.handler=t,this.iframeBind?this.iframeBind():q(this.el,this.arg,this.handler,this.modifiers.capture)}},reset:function(){var t=this.iframeBind?this.el.contentWindow:this.el;this.handler&&Q(t,this.arg,this.handler)},unbind:function(){this.reset()}},rs=["-webkit-","-moz-","-ms-"],ss=["Webkit","Moz","ms"],os=/!important;?$/,as=Object.create(null),hs=null,ls={deep:!0,update:function(t){"string"==typeof t?this.el.style.cssText=t:Di(t)?this.handleObject(t.reduce(v,{})):this.handleObject(t||{})},handleObject:function(t){var e,i,n=this.cache||(this.cache={});for(e in n)e in t||(this.handleSingle(e,null),delete n[e]);for(e in t)i=t[e],i!==n[e]&&(n[e]=i,this.handleSingle(e,i))},handleSingle:function(t,e){if(t=me(t))if(null!=e&&(e+=""),e){var i=os.test(e)?"important":"";i?(e=e.replace(os,"").trim(),this.el.style.setProperty(t.kebab,e,i)):this.el.style[t.camel]=e}else this.el.style[t.camel]=""}},cs="http://www.w3.org/1999/xlink",us=/^xlink:/,fs=/^v-|^:|^@|^(?:is|transition|transition-mode|debounce|track-by|stagger|enter-stagger|leave-stagger)$/,ps=/^(?:value|checked|selected|muted)$/,ds=/^(?:draggable|contenteditable|spellcheck)$/,vs={value:"_value","true-value":"_trueValue","false-value":"_falseValue"},ms={priority:Hr,bind:function(){var t=this.arg,e=this.el.tagName;t||(this.deep=!0);var i=this.descriptor,n=i.interp;n&&(i.hasOneTime&&(this.expression=j(n,this._scope||this.vm)),(fs.test(t)||"name"===t&&("PARTIAL"===e||"SLOT"===e))&&(this.el.removeAttribute(t),this.invalid=!0))},update:function(t){
+if(!this.invalid){var e=this.arg;this.arg?this.handleSingle(e,t):this.handleObject(t||{})}},handleObject:ls.handleObject,handleSingle:function(t,e){var i=this.el,n=this.descriptor.interp;if(this.modifiers.camel&&(t=l(t)),!n&&ps.test(t)&&t in i){var r="value"===t&&null==e?"":e;i[t]!==r&&(i[t]=r)}var s=vs[t];if(!n&&s){i[s]=e;var o=i.__v_model;o&&o.listener()}return"value"===t&&"TEXTAREA"===i.tagName?void i.removeAttribute(t):void(ds.test(t)?i.setAttribute(t,e?"true":"false"):null!=e&&e!==!1?"class"===t?(i.__v_trans&&(e+=" "+i.__v_trans.id+"-transition"),Z(i,e)):us.test(t)?i.setAttributeNS(cs,t,e===!0?"":e):i.setAttribute(t,e===!0?"":e):i.removeAttribute(t))}},gs={priority:Mr,bind:function(){if(this.arg){var t=this.id=l(this.arg),e=(this._scope||this.vm).$els;i(e,t)?e[t]=this.el:kt(e,t,this.el)}},unbind:function(){var t=(this._scope||this.vm).$els;t[this.id]===this.el&&(t[this.id]=null)}},_s={bind:function(){}},ys={bind:function(){var t=this.el;this.vm.$once("pre-hook:compiled",function(){t.removeAttribute("v-cloak")})}},bs={text:kr,html:Dr,"for":qr,"if":Qr,show:Gr,model:es,on:ns,bind:ms,el:gs,ref:_s,cloak:ys},ws={deep:!0,update:function(t){t?"string"==typeof t?this.setClass(t.trim().split(/\s+/)):this.setClass(_e(t)):this.cleanup()},setClass:function(t){this.cleanup(t);for(var e=0,i=t.length;i>e;e++){var n=t[e];n&&ye(this.el,n,X)}this.prevKeys=t},cleanup:function(t){var e=this.prevKeys;if(e)for(var i=e.length;i--;){var n=e[i];(!t||t.indexOf(n)<0)&&ye(this.el,n,Y)}}},Cs={priority:Vr,params:["keep-alive","transition-mode","inline-template"],bind:function(){this.el.__vue__||(this.keepAlive=this.params.keepAlive,this.keepAlive&&(this.cache={}),this.params.inlineTemplate&&(this.inlineTemplate=K(this.el,!0)),this.pendingComponentCb=this.Component=null,this.pendingRemovals=0,this.pendingRemovalCb=null,this.anchor=nt("v-component"),J(this.el,this.anchor),this.el.removeAttribute("is"),this.el.removeAttribute(":is"),this.descriptor.ref&&this.el.removeAttribute("v-ref:"+u(this.descriptor.ref)),this.literal&&this.setComponent(this.expression))},update:function(t){this.literal||this.setComponent(t)},setComponent:function(t,e){if(this.invalidatePending(),t){var i=this;this.resolveComponent(t,function(){i.mountComponent(e)})}else this.unbuild(!0),this.remove(this.childVM,e),this.childVM=null},resolveComponent:function(t,e){var i=this;this.pendingComponentCb=w(function(n){i.ComponentName=n.options.name||("string"==typeof t?t:null),i.Component=n,e()}),this.vm._resolveComponent(t,this.pendingComponentCb)},mountComponent:function(t){this.unbuild(!0);var e=this,i=this.Component.options.activate,n=this.getCached(),r=this.build();i&&!n?(this.waitingFor=r,be(i,r,function(){e.waitingFor===r&&(e.waitingFor=null,e.transition(r,t))})):(n&&r._updateRef(),this.transition(r,t))},invalidatePending:function(){this.pendingComponentCb&&(this.pendingComponentCb.cancel(),this.pendingComponentCb=null)},build:function(t){var e=this.getCached();if(e)return e;if(this.Component){var i={name:this.ComponentName,el:Zt(this.el),template:this.inlineTemplate,parent:this._host||this.vm,_linkerCachable:!this.inlineTemplate,_ref:this.descriptor.ref,_asComponent:!0,_isRouterView:this._isRouterView,_context:this.vm,_scope:this._scope,_frag:this._frag};t&&v(i,t);var n=new this.Component(i);return this.keepAlive&&(this.cache[this.Component.cid]=n),n}},getCached:function(){return this.keepAlive&&this.cache[this.Component.cid]},unbuild:function(t){this.waitingFor&&(this.keepAlive||this.waitingFor.$destroy(),this.waitingFor=null);var e=this.childVM;return!e||this.keepAlive?void(e&&(e._inactive=!0,e._updateRef(!0))):void e.$destroy(!1,t)},remove:function(t,e){var i=this.keepAlive;if(t){this.pendingRemovals++,this.pendingRemovalCb=e;var n=this;t.$remove(function(){n.pendingRemovals--,i||t._cleanup(),!n.pendingRemovals&&n.pendingRemovalCb&&(n.pendingRemovalCb(),n.pendingRemovalCb=null)})}else e&&e()},transition:function(t,e){var i=this,n=this.childVM;switch(n&&(n._inactive=!0),t._inactive=!1,this.childVM=t,i.params.transitionMode){case"in-out":t.$before(i.anchor,function(){i.remove(n,e)});break;case"out-in":i.remove(n,function(){t.$before(i.anchor,e)});break;default:i.remove(n),t.$before(i.anchor,e)}},unbind:function(){if(this.invalidatePending(),this.unbuild(),this.cache){for(var t in this.cache)this.cache[t].$destroy();this.cache=null}}},$s=An._propBindingModes,ks={},xs=/^[$_a-zA-Z]+[\w$]*$/,As=An._propBindingModes,Os={bind:function(){var t=this.vm,e=t._context,i=this.descriptor.prop,n=i.path,r=i.parentPath,s=i.mode===As.TWO_WAY,o=this.parentWatcher=new Ut(e,r,function(e){xe(t,i,e)},{twoWay:s,filters:i.filters,scope:this._scope});if(ke(t,i,o.value),s){var a=this;t.$once("pre-hook:created",function(){a.childWatcher=new Ut(t,n,function(t){o.set(t)},{sync:!0})})}},unbind:function(){this.parentWatcher.teardown(),this.childWatcher&&this.childWatcher.teardown()}},Ts=[],Ns=!1,js="transition",Es="animation",Ss=Ji+"Duration",Fs=Qi+"Duration",Ds=Ri&&window.requestAnimationFrame,Ps=Ds?function(t){Ds(function(){Ds(t)})}:function(t){setTimeout(t,50)},Rs=Se.prototype;Rs.enter=function(t,e){this.cancelPending(),this.callHook("beforeEnter"),this.cb=e,X(this.el,this.enterClass),t(),this.entered=!1,this.callHookWithCb("enter"),this.entered||(this.cancel=this.hooks&&this.hooks.enterCancelled,je(this.enterNextTick))},Rs.enterNextTick=function(){var t=this;this.justEntered=!0,Ps(function(){t.justEntered=!1});var e=this.enterDone,i=this.getCssTransitionType(this.enterClass);this.pendingJsCb?i===js&&Y(this.el,this.enterClass):i===js?(Y(this.el,this.enterClass),this.setupCssCb(qi,e)):i===Es?this.setupCssCb(Gi,e):e()},Rs.enterDone=function(){this.entered=!0,this.cancel=this.pendingJsCb=null,Y(this.el,this.enterClass),this.callHook("afterEnter"),this.cb&&this.cb()},Rs.leave=function(t,e){this.cancelPending(),this.callHook("beforeLeave"),this.op=t,this.cb=e,X(this.el,this.leaveClass),this.left=!1,this.callHookWithCb("leave"),this.left||(this.cancel=this.hooks&&this.hooks.leaveCancelled,this.op&&!this.pendingJsCb&&(this.justEntered?this.leaveDone():je(this.leaveNextTick)))},Rs.leaveNextTick=function(){var t=this.getCssTransitionType(this.leaveClass);if(t){var e=t===js?qi:Gi;this.setupCssCb(e,this.leaveDone)}else this.leaveDone()},Rs.leaveDone=function(){this.left=!0,this.cancel=this.pendingJsCb=null,this.op(),Y(this.el,this.leaveClass),this.callHook("afterLeave"),this.cb&&this.cb(),this.op=null},Rs.cancelPending=function(){this.op=this.cb=null;var t=!1;this.pendingCssCb&&(t=!0,Q(this.el,this.pendingCssEvent,this.pendingCssCb),this.pendingCssEvent=this.pendingCssCb=null),this.pendingJsCb&&(t=!0,this.pendingJsCb.cancel(),this.pendingJsCb=null),t&&(Y(this.el,this.enterClass),Y(this.el,this.leaveClass)),this.cancel&&(this.cancel.call(this.vm,this.el),this.cancel=null)},Rs.callHook=function(t){this.hooks&&this.hooks[t]&&this.hooks[t].call(this.vm,this.el)},Rs.callHookWithCb=function(t){var e=this.hooks&&this.hooks[t];e&&(e.length>1&&(this.pendingJsCb=w(this[t+"Done"])),e.call(this.vm,this.el,this.pendingJsCb))},Rs.getCssTransitionType=function(t){if(!(!qi||document.hidden||this.hooks&&this.hooks.css===!1||Fe(this.el))){var e=this.type||this.typeCache[t];if(e)return e;var i=this.el.style,n=window.getComputedStyle(this.el),r=i[Ss]||n[Ss];if(r&&"0s"!==r)e=js;else{var s=i[Fs]||n[Fs];s&&"0s"!==s&&(e=Es)}return e&&(this.typeCache[t]=e),e}},Rs.setupCssCb=function(t,e){this.pendingCssEvent=t;var i=this,n=this.el,r=this.pendingCssCb=function(s){s.target===n&&(Q(n,t,r),i.pendingCssEvent=i.pendingCssCb=null,!i.pendingJsCb&&e&&e())};q(n,t,r)};var Ls={priority:Ir,update:function(t,e){var i=this.el,n=gt(this.vm.$options,"transitions",t);t=t||"v",e=e||"v",i.__v_trans=new Se(i,t,n,this.vm),Y(i,e+"-transition"),X(i,t+"-transition")}},Hs={style:ls,"class":ws,component:Cs,prop:Os,transition:Ls},Is=/^v-bind:|^:/,Ms=/^v-on:|^@/,Vs=/^v-([^:]+)(?:$|:(.*)$)/,Bs=/\.[^\.]+/g,Ws=/^(v-bind:|:)?transition$/,zs=1e3,Us=2e3;Ye.terminal=!0;var Js=/[^\w\-:\.]/,qs=Object.freeze({compile:De,compileAndLinkProps:Ie,compileRoot:Me,transclude:si,resolveSlots:li}),Qs=/^v-on:|^@/;di.prototype._bind=function(){var t=this.name,e=this.descriptor;if(("cloak"!==t||this.vm._isCompiled)&&this.el&&this.el.removeAttribute){var i=e.attr||"v-"+t;this.el.removeAttribute(i)}var n=e.def;if("function"==typeof n?this.update=n:v(this,n),this._setupParams(),this.bind&&this.bind(),this._bound=!0,this.literal)this.update&&this.update(e.raw);else if((this.expression||this.modifiers)&&(this.update||this.twoWay)&&!this._checkStatement()){var r=this;this.update?this._update=function(t,e){r._locked||r.update(t,e)}:this._update=pi;var s=this._preProcess?p(this._preProcess,this):null,o=this._postProcess?p(this._postProcess,this):null,a=this._watcher=new Ut(this.vm,this.expression,this._update,{filters:this.filters,twoWay:this.twoWay,deep:this.deep,preProcess:s,postProcess:o,scope:this._scope});this.afterBind?this.afterBind():this.update&&this.update(a.value)}},di.prototype._setupParams=function(){if(this.params){var t=this.params;this.params=Object.create(null);for(var e,i,n,r=t.length;r--;)e=u(t[r]),n=l(e),i=M(this.el,e),null!=i?this._setupParamWatcher(n,i):(i=I(this.el,e),null!=i&&(this.params[n]=""===i?!0:i))}},di.prototype._setupParamWatcher=function(t,e){var i=this,n=!1,r=(this._scope||this.vm).$watch(e,function(e,r){if(i.params[t]=e,n){var s=i.paramWatchers&&i.paramWatchers[t];s&&s.call(i,e,r)}else n=!0},{immediate:!0,user:!1});(this._paramUnwatchFns||(this._paramUnwatchFns=[])).push(r)},di.prototype._checkStatement=function(){var t=this.expression;if(t&&this.acceptStatement&&!Mt(t)){var e=It(t).get,i=this._scope||this.vm,n=function(t){i.$event=t,e.call(i,i),i.$event=null};return this.filters&&(n=i._applyFilters(n,null,this.filters)),this.update(n),!0}},di.prototype.set=function(t){this.twoWay&&this._withLock(function(){this._watcher.set(t)})},di.prototype._withLock=function(t){var e=this;e._locked=!0,t.call(e),Yi(function(){e._locked=!1})},di.prototype.on=function(t,e,i){q(this.el,t,e,i),(this._listeners||(this._listeners=[])).push([t,e])},di.prototype._teardown=function(){if(this._bound){this._bound=!1,this.unbind&&this.unbind(),this._watcher&&this._watcher.teardown();var t,e=this._listeners;if(e)for(t=e.length;t--;)Q(this.el,e[t][0],e[t][1]);var i=this._paramUnwatchFns;if(i)for(t=i.length;t--;)i[t]();this.vm=this.el=this._watcher=this._listeners=null}};var Gs=/[^|]\|[^|]/;xt(wi),ui(wi),fi(wi),vi(wi),mi(wi),gi(wi),_i(wi),yi(wi),bi(wi);var Zs={priority:Ur,params:["name"],bind:function(){var t=this.params.name||"default",e=this.vm._slotContents&&this.vm._slotContents[t];e&&e.hasChildNodes()?this.compile(e.cloneNode(!0),this.vm._context,this.vm):this.fallback()},compile:function(t,e,i){if(t&&e){if(this.el.hasChildNodes()&&1===t.childNodes.length&&1===t.childNodes[0].nodeType&&t.childNodes[0].hasAttribute("v-if")){var n=document.createElement("template");n.setAttribute("v-else",""),n.innerHTML=this.el.innerHTML,n._context=this.vm,t.appendChild(n)}var r=i?i._scope:this._scope;this.unlink=e.$compile(t,i,r,this._frag)}t?J(this.el,t):z(this.el)},fallback:function(){this.compile(K(this.el,!0),this.vm)},unbind:function(){this.unlink&&this.unlink()}},Xs={priority:Br,params:["name"],paramWatchers:{name:function(t){Qr.remove.call(this),t&&this.insert(t)}},bind:function(){this.anchor=nt("v-partial"),J(this.el,this.anchor),this.insert(this.params.name)},insert:function(t){var e=gt(this.vm.$options,"partials",t,!0);e&&(this.factory=new se(this.vm,e),Qr.insert.call(this))},unbind:function(){this.frag&&this.frag.destroy()}},Ys={slot:Zs,partial:Xs},Ks=qr._postProcess,to=/(\d{3})(?=\d)/g,eo={orderBy:ki,filterBy:$i,limitBy:Ci,json:{read:function(t,e){return"string"==typeof t?t:JSON.stringify(t,null,arguments.length>1?e:2)},write:function(t){try{return JSON.parse(t)}catch(e){return t}}},capitalize:function(t){return t||0===t?(t=t.toString(),t.charAt(0).toUpperCase()+t.slice(1)):""},uppercase:function(t){return t||0===t?t.toString().toUpperCase():""},lowercase:function(t){return t||0===t?t.toString().toLowerCase():""},currency:function(t,e,i){if(t=parseFloat(t),!isFinite(t)||!t&&0!==t)return"";e=null!=e?e:"$",i=null!=i?i:2;var n=Math.abs(t).toFixed(i),r=i?n.slice(0,-1-i):n,s=r.length%3,o=s>0?r.slice(0,s)+(r.length>3?",":""):"",a=i?n.slice(-1-i):"",h=0>t?"-":"";return h+e+o+r.slice(s).replace(to,"$1,")+a},pluralize:function(t){var e=d(arguments,1),i=e.length;if(i>1){var n=t%10-1;return n in e?e[n]:e[i-1]}return e[0]+(1===t?"":"s")},debounce:function(t,e){return t?(e||(e=300),y(t,e)):void 0}};return Ai(wi),wi.version="1.0.26",setTimeout(function(){An.devtools&&Li&&Li.emit("init",wi)},0),wi});
+//# sourceMappingURL=vue.min.js.map \ No newline at end of file