From 0b89500cfe986c956f2b487d54a6b593031d794c Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 9 Nov 2016 15:57:30 +0000 Subject: [PATCH 001/488] Makes stop button visible in environment page --- app/views/projects/environments/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index bcac73d3698..da8200a5531 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -11,7 +11,7 @@ = render 'projects/environments/external_url', environment: @environment - if can?(current_user, :update_environment, @environment) = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' - - if can?(current_user, :create_deployment, @environment) && @environment.stoppable? + - if can?(current_user, :create_deployment, @environment) = link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post .deployments-container From 13a680e343bdcd905c9134c57202fdec7d436d96 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 9 Nov 2016 16:02:14 +0000 Subject: [PATCH 002/488] Adds CHANGELOG entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8250b9b5cdb..1c85d4aac44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,7 @@ entry. - Return conflict error in label API when title is taken by group label. !7014 - Reduce the overhead to calculate number of open/closed issues and merge requests within the group or project. !7123 - Fix builds tab visibility. !7178 +- Fix delete environment missing button. !7379 - Fix project features default values. !7181 ## 8.13.3 (2016-11-02) From 1494abe982583c564969baaba7daa251ef333156 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 10 Nov 2016 13:59:26 +0100 Subject: [PATCH 003/488] Allow to stop any environment --- .../projects/environments_controller.rb | 10 ++++-- .../projects/merge_requests_controller.rb | 2 +- app/models/environment.rb | 13 ++++--- app/serializers/environment_entity.rb | 2 +- .../projects/environments/_stop.html.haml | 2 +- .../projects/environments/show.html.haml | 2 +- spec/features/environments_spec.rb | 22 +++++++++--- spec/models/environment_spec.rb | 34 +++++++++++++++---- 8 files changed, 66 insertions(+), 21 deletions(-) diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index ea22b2dcc15..bc66823dfc4 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -47,10 +47,14 @@ class Projects::EnvironmentsController < Projects::ApplicationController end def stop - return render_404 unless @environment.stoppable? + return render_404 unless @environment.available? - new_action = @environment.stop!(current_user) - redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action]) + stop_action = @environment.run_stop!(current_user) + if stop_action + redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, stop_action]) + else + redirect_to namespace_project_environment_path(project.namespace, project, @environment) + end end private diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 9f104d903cc..ccba37c9c5c 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -436,7 +436,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController deployment = environment.first_deployment_for(@merge_request.diff_head_commit) stop_url = - if environment.stoppable? && can?(current_user, :create_deployment, environment) + if environment.can_run_stop_action? && can?(current_user, :create_deployment, environment) stop_namespace_project_environment_path(project.namespace, project, environment) end diff --git a/app/models/environment.rb b/app/models/environment.rb index 73f415c0ef0..5c662bbab87 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -85,13 +85,18 @@ class Environment < ActiveRecord::Base external_url.gsub(/\A.*?:\/\//, '') end - def stoppable? + def can_run_stop_action? available? && stop_action.present? end - def stop!(current_user) - return unless stoppable? + def run_stop!(current_user) + return unless available? - stop_action.play(current_user) + if stop_action.present? + stop_action.play(current_user) + else + stop + nil + end end end diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index ee4392cc46d..bfccfd8bb7c 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -7,7 +7,7 @@ class EnvironmentEntity < Grape::Entity expose :external_url expose :environment_type expose :last_deployment, using: DeploymentEntity - expose :stoppable? + expose :can_run_stop_action? expose :environment_url do |environment| namespace_project_environment_url( diff --git a/app/views/projects/environments/_stop.html.haml b/app/views/projects/environments/_stop.html.haml index 69848123c17..b78ad7ee2c7 100644 --- a/app/views/projects/environments/_stop.html.haml +++ b/app/views/projects/environments/_stop.html.haml @@ -1,4 +1,4 @@ -- if can?(current_user, :create_deployment, environment) && environment.stoppable? +- if can?(current_user, :create_deployment, environment) && environment.can_run_stop_action? .inline = link_to stop_namespace_project_environment_path(@project.namespace, @project, environment), method: :post, class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index da8200a5531..992d98cdd96 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -11,7 +11,7 @@ = render 'projects/environments/external_url', environment: @environment - if can?(current_user, :update_environment, @environment) = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' - - if can?(current_user, :create_deployment, @environment) + - if can?(current_user, :create_deployment, @environment) && @environment.available? = link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post .deployments-container diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index b565586ee14..7c9584f6bf1 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -149,6 +149,24 @@ feature 'Environments', feature: true do scenario 'does show no deployments' do expect(page).to have_content('You don\'t have any deployments right now.') end + + context 'for available environment' do + given(:environment) { create(:environment, project: project, state: :available) } + + scenario 'does allow to stop environment' do + click_link('Stop') + + expect(page).to have_content(environment.name.titleize) + end + end + + context 'for stopped environment' do + given(:environment) { create(:environment, project: project, state: :stopped) } + + scenario 'does not shows stop button' do + expect(page).not_to have_link('Stop') + end + end end context 'with deployments' do @@ -175,10 +193,6 @@ feature 'Environments', feature: true do expect(page).to have_link('Re-deploy') end - scenario 'does not show stop button' do - expect(page).not_to have_link('Stop') - end - context 'with manual action' do given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index a94e6d0165f..b860ba2a26c 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -99,8 +99,8 @@ describe Environment, models: true do end end - describe '#stoppable?' do - subject { environment.stoppable? } + describe '#can_run_stop_action?' do + subject { environment.can_run_stop_action? } context 'when no other actions' do it { is_expected.to be_falsey } @@ -129,17 +129,39 @@ describe Environment, models: true do end end - describe '#stop!' do + describe '#run_stop!' do let(:user) { create(:user) } - subject { environment.stop!(user) } + subject { environment.run_stop!(user) } before do - expect(environment).to receive(:stoppable?).and_call_original + expect(environment).to receive(:available?).and_call_original end context 'when no other actions' do - it { is_expected.to be_nil } + context 'environment is available' do + before do + environment.update(state: :available) + end + + it do + subject + + expect(environment).to be_stopped + end + end + + context 'environment is already stopped' do + before do + environment.update(state: :stopped) + end + + it do + subject + + expect(environment).to be_stopped + end + end end context 'when matching action is defined' do From 5f106512be09b6230080c5cff7eb7a7f896284b9 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 10 Nov 2016 16:03:43 +0000 Subject: [PATCH 004/488] Adds tests Adds changelog entry in the correct place --- CHANGELOG.md | 1 - changelogs/unreleased/24147-delete-env-button.yml | 4 ++++ spec/features/environments_spec.rb | 8 ++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/24147-delete-env-button.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c85d4aac44..8250b9b5cdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,7 +93,6 @@ entry. - Return conflict error in label API when title is taken by group label. !7014 - Reduce the overhead to calculate number of open/closed issues and merge requests within the group or project. !7123 - Fix builds tab visibility. !7178 -- Fix delete environment missing button. !7379 - Fix project features default values. !7181 ## 8.13.3 (2016-11-02) diff --git a/changelogs/unreleased/24147-delete-env-button.yml b/changelogs/unreleased/24147-delete-env-button.yml new file mode 100644 index 00000000000..159d9db492f --- /dev/null +++ b/changelogs/unreleased/24147-delete-env-button.yml @@ -0,0 +1,4 @@ +--- +title: Adds back ability to stop all environments +merge_request: 7379 +author: \ No newline at end of file diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index 7c9584f6bf1..eded771201b 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -239,6 +239,14 @@ feature 'Environments', feature: true do end end end + + context 'whitout stop action'do + scenario 'does allow to stop environment' do + click_link('Stop') + + expect(page).to have_content(environment.name.capitalize) + end + end end end end From 9b26a8b6b3bdff6f1f9ca9f844f862ec08fd5ddb Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 10 Nov 2016 16:07:35 +0000 Subject: [PATCH 005/488] Adds missing new line at the end of the file --- changelogs/unreleased/24147-delete-env-button.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/unreleased/24147-delete-env-button.yml b/changelogs/unreleased/24147-delete-env-button.yml index 159d9db492f..14e80cacbfb 100644 --- a/changelogs/unreleased/24147-delete-env-button.yml +++ b/changelogs/unreleased/24147-delete-env-button.yml @@ -1,4 +1,4 @@ --- title: Adds back ability to stop all environments merge_request: 7379 -author: \ No newline at end of file +author: From 8faabdf7d33b575de11b043cfe6698021d33a973 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 11 Nov 2016 10:23:44 +0000 Subject: [PATCH 006/488] Fix rubocop error --- spec/features/environments_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index eded771201b..f75b197f4fe 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -240,7 +240,7 @@ feature 'Environments', feature: true do end end - context 'whitout stop action'do + context 'whitout stop action' do scenario 'does allow to stop environment' do click_link('Stop') From effc6c1703dd397726fefcc930c89c76e0dc4455 Mon Sep 17 00:00:00 2001 From: Mehdy Khoshnoody Date: Mon, 19 Dec 2016 11:33:55 +0000 Subject: [PATCH 007/488] oauth2.md: should use the provider's URL which is gitlab.example.com --- doc/api/oauth2.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index 5ef5e3f5744..eab532af594 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -57,7 +57,7 @@ Once you have the authorization code you can request an `access_token` using the ``` parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI' -RestClient.post 'http://localhost:3000/oauth/token', parameters +RestClient.post 'http://gitlab.example.com/oauth/token', parameters # The response will be { @@ -77,13 +77,13 @@ You can now make requests to the API with the access token returned. The access token allows you to make requests to the API on a behalf of a user. ``` -GET https://localhost:3000/api/v3/user?access_token=OAUTH-TOKEN +GET https://gitlab.example.com/api/v3/user?access_token=OAUTH-TOKEN ``` Or you can put the token to the Authorization header: ``` -curl --header "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/user +curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/user ``` ## Resource Owner Password Credentials From 206efcdac47553668e961728f82f2b7204d21acf Mon Sep 17 00:00:00 2001 From: Bruno Melli Date: Wed, 21 Dec 2016 23:14:22 -0700 Subject: [PATCH 008/488] Fix the curl command for deleting files (DELETE instead of PUT) --- doc/api/repository_files.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index 8a6baed5987..c6ad78754a1 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -113,7 +113,7 @@ DELETE /projects/:id/repository/files ``` ```bash -curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file' +curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file' ``` Example response: From 7dba8cd2036c0bea0636055835b4f81fe79966af Mon Sep 17 00:00:00 2001 From: Marcel Huber Date: Thu, 22 Dec 2016 09:36:49 +0000 Subject: [PATCH 009/488] Shows how to reference a line within a repository file. --- doc/user/markdown.md | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/doc/user/markdown.md b/doc/user/markdown.md index f6484688721..a2c729267e2 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -237,23 +237,24 @@ GFM will turn that reference into a link so you can navigate between them easily GFM will recognize the following: -| input | references | -|:-----------------------|:--------------------------- | -| `@user_name` | specific user | -| `@group_name` | specific group | -| `@all` | entire team | -| `#123` | issue | -| `!123` | merge request | -| `$123` | snippet | -| `~123` | label by ID | -| `~bug` | one-word label by name | -| `~"feature request"` | multi-word label by name | -| `%123` | milestone by ID | -| `%v1.23` | one-word milestone by name | -| `%"release candidate"` | multi-word milestone by name | -| `9ba12248` | specific commit | -| `9ba12248...b19a04f5` | commit range comparison | -| `[README](doc/README)` | repository file references | +| input | references | +|:---------------------------|:--------------------------------| +| `@user_name` | specific user | +| `@group_name` | specific group | +| `@all` | entire team | +| `#123` | issue | +| `!123` | merge request | +| `$123` | snippet | +| `~123` | label by ID | +| `~bug` | one-word label by name | +| `~"feature request"` | multi-word label by name | +| `%123` | milestone by ID | +| `%v1.23` | one-word milestone by name | +| `%"release candidate"` | multi-word milestone by name | +| `9ba12248` | specific commit | +| `9ba12248...b19a04f5` | commit range comparison | +| `[README](doc/README)` | repository file references | +| `[README](doc/README#L13)` | repository file line references | GFM also recognizes certain cross-project references: From 8bf52a4ae3aebc8c58f51cff696e99ecafe9c7c8 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Fri, 16 Dec 2016 02:12:21 -0200 Subject: [PATCH 010/488] Show directory hierarchy when listing wiki pages --- app/controllers/projects/wikis_controller.rb | 3 ++- app/models/wiki_page.rb | 25 +++++++++++++++++++ app/views/projects/wikis/_sidebar.html.haml | 12 ++++++--- app/views/projects/wikis/pages.html.haml | 14 +++++++---- .../23535-folders-in-wiki-repository.yml | 4 +++ spec/models/wiki_page_spec.rb | 17 +++++++++++++ 6 files changed, 65 insertions(+), 10 deletions(-) create mode 100644 changelogs/unreleased/23535-folders-in-wiki-repository.yml diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index c3353446fd1..45a42400b2a 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -8,6 +8,7 @@ class Projects::WikisController < Projects::ApplicationController def pages @wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]) + @wiki_directories = WikiPage.group_by_directory(@wiki_pages) end def show @@ -116,7 +117,7 @@ class Projects::WikisController < Projects::ApplicationController # Call #wiki to make sure the Wiki Repo is initialized @project_wiki.wiki - @sidebar_wiki_pages = @project_wiki.pages.first(15) + @sidebar_wiki_directories = WikiPage.group_by_directory(@project_wiki.pages.first(15)) rescue ProjectWiki::CouldNotCreateWikiError flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." redirect_to project_path(@project) diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index c3de278f5b7..30db2b13dc0 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -12,6 +12,23 @@ class WikiPage ActiveModel::Name.new(self, nil, 'wiki') end + def self.group_by_directory(pages) + directories = {} + + pages.each do |page| + if page.slug.include?('/') + # Directory hierarchy is given by matching from the beginning up to + # the last forward slash. + directory = page.slug.match(/\A(.+)\//)[1] + directories[directory] = add_to_directory(directories[directory], page) + else + directories['root'] = add_to_directory(directories['root'], page) + end + end + + directories + end + def to_key [:slug] end @@ -176,6 +193,14 @@ class WikiPage private + def self.add_to_directory(directory, page) + if directory.present? + directory << page + else + [page] + end + end + def set_attributes attributes[:slug] = @page.url_path attributes[:title] = @page.title diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index cad9c15a49e..5aee1a136f5 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -12,10 +12,14 @@ .blocks-container .block.block-first %ul.wiki-pages - - @sidebar_wiki_pages.each do |wiki_page| - %li{ class: params[:id] == wiki_page.slug ? 'active' : '' } - = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do - = wiki_page.title.capitalize + - @sidebar_wiki_directories.each do |wiki_directory, wiki_pages| + %li + = wiki_directory + %ul + - wiki_pages.each do |wiki_page| + %li{ class: params[:id] == wiki_page.slug ? 'active' : '' } + = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do + = wiki_page.title.capitalize .block = link_to namespace_project_wikis_pages_path(@project.namespace, @project), class: 'btn btn-block' do More Pages diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index e1eaffc6884..274afb1bdea 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -14,10 +14,14 @@ Clone repository %ul.content-list - - @wiki_pages.each do |wiki_page| + - @wiki_directories.each do |wiki_directory, wiki_pages| %li - = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page) - %small (#{wiki_page.format}) - .pull-right - %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)} + = wiki_directory + %ul + - wiki_pages.each do |wiki_page| + %li + = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page) + %small (#{wiki_page.format}) + .pull-right + %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)} = paginate @wiki_pages, theme: 'gitlab' diff --git a/changelogs/unreleased/23535-folders-in-wiki-repository.yml b/changelogs/unreleased/23535-folders-in-wiki-repository.yml new file mode 100644 index 00000000000..7361b182a94 --- /dev/null +++ b/changelogs/unreleased/23535-folders-in-wiki-repository.yml @@ -0,0 +1,4 @@ +--- +title: Show directory hierarchy when listing wiki pages +merge_request: +author: Alex Braha Stoll diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 5c34b1b0a30..25e7b517fe6 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -7,6 +7,23 @@ describe WikiPage, models: true do subject { WikiPage.new(wiki) } + describe '::group_by_directory' do + context 'when there are no pages' do + it 'returns an empty hash' do + end + end + + context 'when there are pages' do + let!(:page_1) { create_page('page_1', 'content') } + let!(:page_2) { create_page('directory/page_2', 'content') } + let(:pages) { [page_1, page_2] } + + xit 'returns a hash in which keys are directories and values are their pages' do + expected_grouped_pages = { 'root' => [page_1], 'directory' => [page_2] } + end + end + end + describe "#initialize" do context "when initialized with an existing gollum page" do before do From 083442bc716d7e69cbb9e7852159b0f3ba9a4610 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Sat, 17 Dec 2016 16:38:26 -0200 Subject: [PATCH 011/488] Add specs for WikiPage.group_by_directory --- spec/models/wiki_page_spec.rb | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 25e7b517fe6..595d4a621c1 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -7,19 +7,37 @@ describe WikiPage, models: true do subject { WikiPage.new(wiki) } - describe '::group_by_directory' do + describe '.group_by_directory' do context 'when there are no pages' do it 'returns an empty hash' do + expect(WikiPage.group_by_directory(nil)).to eq({}) + expect(WikiPage.group_by_directory([])).to eq({}) end end context 'when there are pages' do - let!(:page_1) { create_page('page_1', 'content') } - let!(:page_2) { create_page('directory/page_2', 'content') } - let(:pages) { [page_1, page_2] } + before do + create_page('page_1', 'content') + create_page('dir_1/page_2', 'content') + create_page('dir_1/dir_2/page_3', 'content') + end - xit 'returns a hash in which keys are directories and values are their pages' do - expected_grouped_pages = { 'root' => [page_1], 'directory' => [page_2] } + it 'returns a hash in which keys are directories and values are their pages' do + page_1 = wiki.find_page('page_1') + page_2 = wiki.find_page('dir_1/page_2') + page_3 = wiki.find_page('dir_1/dir_2/page_3') + expected_grouped_pages = { + '/' => [page_1], 'dir_1' => [page_2], 'dir_1/dir_2' => [page_3] + } + + grouped_pages = WikiPage.group_by_directory(wiki.pages) + + grouped_pages.each do |dir, pages| + expected_slugs = expected_grouped_pages.fetch(dir).map(&:slug) + slugs = pages.map(&:slug) + + expect(slugs).to match_array(expected_slugs) + end end end end From bebfba3e6de520f98d263ced2d2a17f6ddfc4a6f Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Sat, 17 Dec 2016 16:38:55 -0200 Subject: [PATCH 012/488] Refactor WikiPage.group_by_directory --- app/models/wiki_page.rb | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 30db2b13dc0..425384d3df4 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -13,17 +13,14 @@ class WikiPage end def self.group_by_directory(pages) - directories = {} + return {} if pages.blank? + directories = { '/' => [] } pages.each do |page| - if page.slug.include?('/') - # Directory hierarchy is given by matching from the beginning up to - # the last forward slash. - directory = page.slug.match(/\A(.+)\//)[1] - directories[directory] = add_to_directory(directories[directory], page) - else - directories['root'] = add_to_directory(directories['root'], page) - end + directory = page.wiki.page_title_and_dir(page.slug).last + directory = '/' if directory.blank? + directories[directory] ||= [] + directories[directory] << page end directories From c7294dded2fc869d6431ac192649f11ca7e96375 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Sat, 17 Dec 2016 16:39:55 -0200 Subject: [PATCH 013/488] Remove WikiPage.add_to_directory --- app/models/wiki_page.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 425384d3df4..aeacb6f8995 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -190,14 +190,6 @@ class WikiPage private - def self.add_to_directory(directory, page) - if directory.present? - directory << page - else - [page] - end - end - def set_attributes attributes[:slug] = @page.url_path attributes[:title] = @page.title From 91e1701b6abfb9a4b9ad50996bf4383d63b97e74 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Sat, 17 Dec 2016 17:38:45 -0200 Subject: [PATCH 014/488] Remove root directory name from the sidebar of wikis --- app/views/projects/wikis/_sidebar.html.haml | 16 +++++++++------- .../projects/wikis/_sidebar_wiki_pages.html.haml | 4 ++++ 2 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 app/views/projects/wikis/_sidebar_wiki_pages.html.haml diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index 5aee1a136f5..b7464180a0c 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -13,13 +13,15 @@ .block.block-first %ul.wiki-pages - @sidebar_wiki_directories.each do |wiki_directory, wiki_pages| - %li - = wiki_directory - %ul - - wiki_pages.each do |wiki_page| - %li{ class: params[:id] == wiki_page.slug ? 'active' : '' } - = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do - = wiki_page.title.capitalize + - if wiki_directory == '/' + = render 'sidebar_wiki_pages', wiki_pages: wiki_pages + - else + %li + = wiki_directory + %ul + = render 'sidebar_wiki_pages', wiki_pages: wiki_pages + + .block = link_to namespace_project_wikis_pages_path(@project.namespace, @project), class: 'btn btn-block' do More Pages diff --git a/app/views/projects/wikis/_sidebar_wiki_pages.html.haml b/app/views/projects/wikis/_sidebar_wiki_pages.html.haml new file mode 100644 index 00000000000..65453a384d2 --- /dev/null +++ b/app/views/projects/wikis/_sidebar_wiki_pages.html.haml @@ -0,0 +1,4 @@ +- wiki_pages.each do |wiki_page| + %li{ class: params[:id] == wiki_page.slug ? 'active' : '' } + = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do + = wiki_page.title.capitalize From 294acf1c5cd2aea353081059c60b3951a2cf7c77 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Sat, 17 Dec 2016 18:00:29 -0200 Subject: [PATCH 015/488] Remove root directory name from index of wikis --- app/views/projects/wikis/_wiki_pages.html.haml | 6 ++++++ app/views/projects/wikis/pages.html.haml | 17 ++++++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 app/views/projects/wikis/_wiki_pages.html.haml diff --git a/app/views/projects/wikis/_wiki_pages.html.haml b/app/views/projects/wikis/_wiki_pages.html.haml new file mode 100644 index 00000000000..ac98599d96b --- /dev/null +++ b/app/views/projects/wikis/_wiki_pages.html.haml @@ -0,0 +1,6 @@ +- wiki_pages.each do |wiki_page| + %li + = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page) + %small (#{wiki_page.format}) + .pull-right + %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)} diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index 274afb1bdea..2813b3a1c81 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -15,13 +15,12 @@ %ul.content-list - @wiki_directories.each do |wiki_directory, wiki_pages| - %li - = wiki_directory - %ul - - wiki_pages.each do |wiki_page| - %li - = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page) - %small (#{wiki_page.format}) - .pull-right - %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)} + - if wiki_directory == '/' + = render 'wiki_pages', wiki_pages: wiki_pages + - else + %li + = wiki_directory + %ul + = render 'wiki_pages', wiki_pages: wiki_pages + = paginate @wiki_pages, theme: 'gitlab' From 5bbe6559917e1e64cdb047b6235715e2a7f002f2 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Sun, 18 Dec 2016 21:22:20 -0200 Subject: [PATCH 016/488] Add component to show the full path of a wiki page when viewing its content --- app/assets/stylesheets/pages/wiki.scss | 3 +- app/models/wiki_page.rb | 11 +++++++ app/views/projects/wikis/show.html.haml | 2 +- spec/models/wiki_page_spec.rb | 40 +++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index b9f81533150..7afadb7364d 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -14,7 +14,8 @@ font-size: 22px; } - .wiki-last-edit-by { + .wiki-last-edit-by, .wiki-page-full-path { + display: block; color: $gl-gray-light; strong { diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index aeacb6f8995..e970cfbfff8 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -88,6 +88,12 @@ class WikiPage end end + # The hierarchy of the directory this page is contained in. + def directory + dir = wiki.page_title_and_dir(slug).last + dir.present? ? dir : '/' + end + # The processed/formatted content of this page. def formatted_content @attributes[:formatted_content] ||= if @page @@ -100,6 +106,11 @@ class WikiPage @attributes[:format] || :markdown end + # The full path for this page, including its filename and extension. + def full_path + "/#{directory}/#{page.filename}".gsub(/\/+/, '/') + end + # The commit message for this page version. def message version.try(:message) diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 1b6dceee241..25ae5c587ec 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -8,7 +8,7 @@ .nav-text %h2.wiki-page-title= @page.title.capitalize - + %span.wiki-page-full-path= "(#{@page.full_path})" %span.wiki-last-edit-by Last edited by %strong diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 595d4a621c1..c40a89b9dfb 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -224,6 +224,46 @@ describe WikiPage, models: true do end end + describe '#directory' do + context 'when the page is at the root directory' do + it 'returns /' do + create_page('file', 'content') + page = wiki.find_page('file') + + expect(page.directory).to eq('/') + end + end + + context 'when the page is inside an actual directory' do + it 'returns the full directory hierarchy' do + create_page('dir_1/dir_1_1/file', 'content') + page = wiki.find_page('dir_1/dir_1_1/file') + + expect(page.directory).to eq('dir_1/dir_1_1') + end + end + end + + describe '#full_path' do + context 'when the page is at the root directory' do + it 'returns /filename.fileextension' do + create_page('file', 'content') + page = wiki.find_page('file') + + expect(page.full_path).to eq('/file.md') + end + end + + context 'when the page is inside an actual directory' do + it 'returns /directory/filename.fileextension' do + create_page('dir/file', 'content') + page = wiki.find_page('dir/file') + + expect(page.full_path).to eq('/dir/file.md') + end + end + end + describe '#historical?' do before do create_page('Update', 'content') From 904aa039e5ccb4d9f653d254ea5818be130fb218 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Sun, 18 Dec 2016 21:27:30 -0200 Subject: [PATCH 017/488] Change WikiPage.group_by_directory to use WikiPage#directory --- app/models/wiki_page.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index e970cfbfff8..1dbb3407623 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -17,10 +17,8 @@ class WikiPage directories = { '/' => [] } pages.each do |page| - directory = page.wiki.page_title_and_dir(page.slug).last - directory = '/' if directory.blank? - directories[directory] ||= [] - directories[directory] << page + directories[page.directory] ||= [] + directories[page.directory] << page end directories From 5607bb8f0921cbfa4586bb7b92acb6666a65b4e2 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Sun, 18 Dec 2016 21:37:10 -0200 Subject: [PATCH 018/488] Change WikiPage#directory to always start a directory hierarchy with '/' --- app/models/wiki_page.rb | 4 ++-- spec/models/wiki_page_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 1dbb3407623..a563b0b7a72 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -89,7 +89,7 @@ class WikiPage # The hierarchy of the directory this page is contained in. def directory dir = wiki.page_title_and_dir(slug).last - dir.present? ? dir : '/' + "/#{dir}" end # The processed/formatted content of this page. @@ -106,7 +106,7 @@ class WikiPage # The full path for this page, including its filename and extension. def full_path - "/#{directory}/#{page.filename}".gsub(/\/+/, '/') + "#{directory}/#{page.filename}".gsub(/\/+/, '/') end # The commit message for this page version. diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index c40a89b9dfb..91d5fccce60 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -27,7 +27,7 @@ describe WikiPage, models: true do page_2 = wiki.find_page('dir_1/page_2') page_3 = wiki.find_page('dir_1/dir_2/page_3') expected_grouped_pages = { - '/' => [page_1], 'dir_1' => [page_2], 'dir_1/dir_2' => [page_3] + '/' => [page_1], '/dir_1' => [page_2], '/dir_1/dir_2' => [page_3] } grouped_pages = WikiPage.group_by_directory(wiki.pages) @@ -239,7 +239,7 @@ describe WikiPage, models: true do create_page('dir_1/dir_1_1/file', 'content') page = wiki.find_page('dir_1/dir_1_1/file') - expect(page.directory).to eq('dir_1/dir_1_1') + expect(page.directory).to eq('/dir_1/dir_1_1') end end end From 7f914ec73f9bdb3d2d4e51d48906a36186f496e3 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Sun, 18 Dec 2016 22:30:07 -0200 Subject: [PATCH 019/488] Add tip about specifying the full path when creating new wiki pages --- app/assets/stylesheets/pages/wiki.scss | 8 ++++++++ app/views/projects/wikis/_new.html.haml | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 7afadb7364d..6423c7d6302 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -1,3 +1,11 @@ +.new-wiki-page { + .new-wiki-page-slug-tip { + display: inline-block; + max-width: 100%; + margin-top: 5px; + } +} + .title .edit-wiki-header { width: 780px; margin-left: auto; diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index c74f53b4c39..f9f8fc63288 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -13,5 +13,9 @@ = label_tag :new_wiki_path do %span Page slug = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project), autofocus: true + %span.new-wiki-page-slug-tip + %i.fa.fa-lightbulb-o +  Tip: You can specify the full path for the new file. + We will automatically create any missing directories. .form-actions = button_tag 'Create Page', class: 'build-new-wiki btn btn-create' From f25344e36e9c1b0d0df2211b82b26c6515e96c31 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Mon, 19 Dec 2016 02:34:35 -0200 Subject: [PATCH 020/488] Change WikiPage.group_by_directory to order by directory and file alphabetical order --- app/models/wiki_page.rb | 29 ++++++++++++++++++++++++++++- spec/models/wiki_page_spec.rb | 30 ++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index a563b0b7a72..a84f84c67cd 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -12,10 +12,17 @@ class WikiPage ActiveModel::Name.new(self, nil, 'wiki') end + # Sorts and groups pages by directory. + # + # pages - an array of WikiPage objects. + # + # Returns a hash whose keys are directories and whose values are WikiPage + # arrays. See WikiPage.sort_by_directory for more info about the ordering. def self.group_by_directory(pages) return {} if pages.blank? - directories = { '/' => [] } + pages = sort_by_directory(pages) + directories = {} pages.each do |page| directories[page.directory] ||= [] directories[page.directory] << page @@ -199,6 +206,26 @@ class WikiPage private + # Sorts an array of pages by directory and file alphabetical order. + # Pages at the root directory will come first. The next pages will be + # sorted by their directories. Within directories, pages are sorted by + # filename alphabetical order. Pages are sorted in such a fashion that + # nested directories will always follow their parents (e.g. pages in + # dir_1/nested_dir_1 will follow pages inside dir_1). + # + # pages - an array of WikiPage objects. + # + # Returns a sorted array of WikiPage objects. + def self.sort_by_directory(pages) + pages.sort do |page, next_page| + if page.directory == next_page.directory + page.slug <=> next_page.slug + else + page.directory <=> next_page.directory + end + end + end + def set_attributes attributes[:slug] = @page.url_path attributes[:title] = @page.title diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 91d5fccce60..374849e1932 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -17,17 +17,22 @@ describe WikiPage, models: true do context 'when there are pages' do before do - create_page('page_1', 'content') + create_page('dir_1/dir_1_1/page_3', 'content') create_page('dir_1/page_2', 'content') - create_page('dir_1/dir_2/page_3', 'content') + create_page('dir_2/page_5', 'content') + create_page('dir_2/page_4', 'content') + create_page('page_1', 'content') end it 'returns a hash in which keys are directories and values are their pages' do page_1 = wiki.find_page('page_1') page_2 = wiki.find_page('dir_1/page_2') - page_3 = wiki.find_page('dir_1/dir_2/page_3') + page_3 = wiki.find_page('dir_1/dir_1_1/page_3') + page_4 = wiki.find_page('dir_2/page_4') + page_5 = wiki.find_page('dir_2/page_5') expected_grouped_pages = { - '/' => [page_1], '/dir_1' => [page_2], '/dir_1/dir_2' => [page_3] + '/' => [page_1], '/dir_1' => [page_2], '/dir_1/dir_1_1' => [page_3], + '/dir_2' => [page_4, page_5] } grouped_pages = WikiPage.group_by_directory(wiki.pages) @@ -39,6 +44,23 @@ describe WikiPage, models: true do expect(slugs).to match_array(expected_slugs) end end + + it 'returns a hash in which keys (directories) are sorted by alphabetical position' do + expected_ordered_directories = ['/', '/dir_1', '/dir_1/dir_1_1', '/dir_2'] + + grouped_pages = WikiPage.group_by_directory(wiki.pages) + + expect(grouped_pages.keys).to eq(expected_ordered_directories) + end + + it 'returns a hash in which values (pages) are sorted by alphabetical position' do + expected_ordered_page_slugs = ['dir_2/page_4', 'dir_2/page_5'] + + grouped_pages = WikiPage.group_by_directory(wiki.pages) + + dir_2_page_slugs = grouped_pages.fetch('/dir_2').map(&:slug) + expect(dir_2_page_slugs).to eq(expected_ordered_page_slugs) + end end end From e66fa4105b4ae275dc76a6594346367bd32b5ce9 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Mon, 19 Dec 2016 03:13:26 -0200 Subject: [PATCH 021/488] Improve style of wiki page lists --- app/assets/stylesheets/pages/wiki.scss | 16 ++++++++++++++++ app/views/projects/wikis/pages.html.haml | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 6423c7d6302..819e4c3e3d8 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -130,6 +130,10 @@ margin: 5px 0 10px; } + ul.wiki-pages ul { + padding-left: 15px; + } + .wiki-sidebar-header { padding: 0 $gl-padding $gl-padding; @@ -138,3 +142,15 @@ } } } + +ul.wiki-pages-list.content-list { + & ul { + list-style: none; + margin-left: 0; + padding-left: 15px; + } + + & ul li { + padding: 5px 0; + } +} diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index 2813b3a1c81..28dd81e5c3f 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -13,7 +13,7 @@ = icon('cloud-download') Clone repository - %ul.content-list + %ul.wiki-pages-list.content-list - @wiki_directories.each do |wiki_directory, wiki_pages| - if wiki_directory == '/' = render 'wiki_pages', wiki_pages: wiki_pages From 50e3e796ea5ca12addbfb438feae606ca7067a22 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Tue, 20 Dec 2016 12:15:56 -0200 Subject: [PATCH 022/488] Fix scss style violation --- app/assets/stylesheets/pages/wiki.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 819e4c3e3d8..1a22cd7d33f 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -22,7 +22,8 @@ font-size: 22px; } - .wiki-last-edit-by, .wiki-page-full-path { + .wiki-page-full-path, + .wiki-last-edit-by { display: block; color: $gl-gray-light; From 645aaf6e3d18007b56e5bbb33c6fda1526bdc716 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Sun, 25 Dec 2016 22:13:20 -0200 Subject: [PATCH 023/488] Use the icon helper at wikis/_new.html.haml --- app/views/projects/wikis/_new.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index f9f8fc63288..d91a7096701 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -14,8 +14,8 @@ %span Page slug = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project), autofocus: true %span.new-wiki-page-slug-tip - %i.fa.fa-lightbulb-o -  Tip: You can specify the full path for the new file. + =icon('lightbulb-o') + Tip: You can specify the full path for the new file. We will automatically create any missing directories. .form-actions = button_tag 'Create Page', class: 'build-new-wiki btn btn-create' From 77fe503a1fd01eaa8b790d1aacc0cdab159f015e Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Sun, 25 Dec 2016 22:50:36 -0200 Subject: [PATCH 024/488] Remove WikiPage.sort_by_directory --- app/models/wiki_page.rb | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index a84f84c67cd..efb6ff9bf2b 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -17,10 +17,10 @@ class WikiPage # pages - an array of WikiPage objects. # # Returns a hash whose keys are directories and whose values are WikiPage - # arrays. See WikiPage.sort_by_directory for more info about the ordering. + # arrays. def self.group_by_directory(pages) return {} if pages.blank? - pages = sort_by_directory(pages) + pages = pages.sort_by { |page| [page.directory, page.slug] } directories = {} pages.each do |page| @@ -206,26 +206,6 @@ class WikiPage private - # Sorts an array of pages by directory and file alphabetical order. - # Pages at the root directory will come first. The next pages will be - # sorted by their directories. Within directories, pages are sorted by - # filename alphabetical order. Pages are sorted in such a fashion that - # nested directories will always follow their parents (e.g. pages in - # dir_1/nested_dir_1 will follow pages inside dir_1). - # - # pages - an array of WikiPage objects. - # - # Returns a sorted array of WikiPage objects. - def self.sort_by_directory(pages) - pages.sort do |page, next_page| - if page.directory == next_page.directory - page.slug <=> next_page.slug - else - page.directory <=> next_page.directory - end - end - end - def set_attributes attributes[:slug] = @page.url_path attributes[:title] = @page.title From 8d8c5d9f61491c63e89d73a3f77244d3cd6406da Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Sun, 25 Dec 2016 23:05:04 -0200 Subject: [PATCH 025/488] Simplify WikiPage.group_by_directory by using Enumerable#group_by --- app/models/wiki_page.rb | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index efb6ff9bf2b..0e905cb9a00 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -20,15 +20,8 @@ class WikiPage # arrays. def self.group_by_directory(pages) return {} if pages.blank? - pages = pages.sort_by { |page| [page.directory, page.slug] } - - directories = {} - pages.each do |page| - directories[page.directory] ||= [] - directories[page.directory] << page - end - - directories + pages.sort_by { |page| [page.directory, page.slug] }. + group_by { |page| page.directory } end def to_key From b361a67fb019e5c7f5361bbd3c43545da3ab0288 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Mon, 26 Dec 2016 13:07:40 -0200 Subject: [PATCH 026/488] Add model WikiDirectory --- app/models/wiki_directory.rb | 13 +++++++++ spec/factories/wiki_directories.rb | 6 ++++ spec/models/wiki_directory_spec.rb | 45 ++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 app/models/wiki_directory.rb create mode 100644 spec/factories/wiki_directories.rb create mode 100644 spec/models/wiki_directory_spec.rb diff --git a/app/models/wiki_directory.rb b/app/models/wiki_directory.rb new file mode 100644 index 00000000000..c126a4d0421 --- /dev/null +++ b/app/models/wiki_directory.rb @@ -0,0 +1,13 @@ +class WikiDirectory + include ActiveModel::Validations + + attr_accessor :slug, :pages, :directories + + validates :slug, presence: true + + def initialize(slug, pages = [], directories = []) + @slug = slug + @pages = pages + @directories = directories + end +end diff --git a/spec/factories/wiki_directories.rb b/spec/factories/wiki_directories.rb new file mode 100644 index 00000000000..3f3c864ac2b --- /dev/null +++ b/spec/factories/wiki_directories.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :wiki_directory do + slug '/path_up_to/dir' + initialize_with { new(slug) } + end +end diff --git a/spec/models/wiki_directory_spec.rb b/spec/models/wiki_directory_spec.rb new file mode 100644 index 00000000000..8362a285c54 --- /dev/null +++ b/spec/models/wiki_directory_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +RSpec.describe WikiDirectory, models: true do + describe 'validations' do + subject { build(:wiki_directory) } + + it { is_expected.to validate_presence_of(:slug) } + end + + describe '#initialize' do + context 'when there are pages and directories' do + let(:pages) { [build(:wiki_page)] } + let(:other_directories) { [build(:wiki_directory)] } + let(:directory) { WikiDirectory.new('/path_up_to/dir', pages, other_directories) } + + it 'sets the slug attribute' do + expect(directory.slug).to eq('/path_up_to/dir') + end + + it 'sets the pages attribute' do + expect(directory.pages).to eq(pages) + end + + it 'sets the directories attribute' do + expect(directory.directories).to eq(other_directories) + end + end + + context 'when there are no pages or directories' do + let(:directory) { WikiDirectory.new('/path_up_to/dir') } + + it 'sets the slug attribute' do + expect(directory.slug).to eq('/path_up_to/dir') + end + + it 'sets the pages attribute to an empty array' do + expect(directory.pages).to eq([]) + end + + it 'sets the directories attribute to an empty array' do + expect(directory.directories).to eq([]) + end + end + end +end From c8a1e9682656b6b3ec714e38459e089df2ee106c Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Mon, 26 Dec 2016 20:12:15 -0200 Subject: [PATCH 027/488] Change WikiPage.group_by_directory to use WikiDirectory --- app/models/wiki_page.rb | 18 +++++++-- spec/models/wiki_page_spec.rb | 72 ++++++++++++++++++++--------------- 2 files changed, 56 insertions(+), 34 deletions(-) diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 0e905cb9a00..63e5aa0e519 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -16,12 +16,22 @@ class WikiPage # # pages - an array of WikiPage objects. # - # Returns a hash whose keys are directories and whose values are WikiPage - # arrays. + # Returns an array of WikiPage and WikiDirectory objects. The entries are + # sorted by alphabetical order (directories and pages inside each directory). + # Pages at the root level come before everything. def self.group_by_directory(pages) - return {} if pages.blank? + return [] if pages.blank? + pages.sort_by { |page| [page.directory, page.slug] }. - group_by { |page| page.directory } + group_by { |page| page.directory }. + map do |dir, pages| + if dir == '/' + pages + else + WikiDirectory.new(dir, pages) + end + end. + flatten end def to_key diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 374849e1932..9eb94cb028d 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -9,9 +9,9 @@ describe WikiPage, models: true do describe '.group_by_directory' do context 'when there are no pages' do - it 'returns an empty hash' do - expect(WikiPage.group_by_directory(nil)).to eq({}) - expect(WikiPage.group_by_directory([])).to eq({}) + it 'returns an empty array' do + expect(WikiPage.group_by_directory(nil)).to eq([]) + expect(WikiPage.group_by_directory([])).to eq([]) end end @@ -23,43 +23,47 @@ describe WikiPage, models: true do create_page('dir_2/page_4', 'content') create_page('page_1', 'content') end + let(:page_1) { wiki.find_page('page_1') } + let(:dir_1) do + WikiDirectory.new('dir_1', [wiki.find_page('dir_1/page_2')]) + end + let(:dir_1_1) do + WikiDirectory.new('dir_1/dir_1_1', [wiki.find_page('dir_1/dir_1_1/page_3')]) + end + let(:dir_2) do + pages = [wiki.find_page('dir_2/page_5'), + wiki.find_page('dir_2/page_4')] + WikiDirectory.new('dir_2', pages) + end - it 'returns a hash in which keys are directories and values are their pages' do - page_1 = wiki.find_page('page_1') - page_2 = wiki.find_page('dir_1/page_2') - page_3 = wiki.find_page('dir_1/dir_1_1/page_3') - page_4 = wiki.find_page('dir_2/page_4') - page_5 = wiki.find_page('dir_2/page_5') - expected_grouped_pages = { - '/' => [page_1], '/dir_1' => [page_2], '/dir_1/dir_1_1' => [page_3], - '/dir_2' => [page_4, page_5] - } + it 'returns an array with pages and directories' do + expected_grouped_entries = [page_1, dir_1, dir_1_1, dir_2] - grouped_pages = WikiPage.group_by_directory(wiki.pages) + grouped_entries = WikiPage.group_by_directory(wiki.pages) - grouped_pages.each do |dir, pages| - expected_slugs = expected_grouped_pages.fetch(dir).map(&:slug) - slugs = pages.map(&:slug) + grouped_entries.each_with_index do |page_or_dir, i| + expected_page_or_dir = expected_grouped_entries[i] + expected_slugs = get_slugs(expected_page_or_dir) + slugs = get_slugs(page_or_dir) expect(slugs).to match_array(expected_slugs) end end - it 'returns a hash in which keys (directories) are sorted by alphabetical position' do - expected_ordered_directories = ['/', '/dir_1', '/dir_1/dir_1_1', '/dir_2'] + it 'returns an array sorted by alphabetical position' do + # Directories and pages within directories are sorted alphabetically. + # Pages at root come before everything. + expected_order = ['page_1', 'dir_1/page_2', 'dir_1/dir_1_1/page_3', + 'dir_2/page_4', 'dir_2/page_5'] - grouped_pages = WikiPage.group_by_directory(wiki.pages) + grouped_entries = WikiPage.group_by_directory(wiki.pages) - expect(grouped_pages.keys).to eq(expected_ordered_directories) - end - - it 'returns a hash in which values (pages) are sorted by alphabetical position' do - expected_ordered_page_slugs = ['dir_2/page_4', 'dir_2/page_5'] - - grouped_pages = WikiPage.group_by_directory(wiki.pages) - - dir_2_page_slugs = grouped_pages.fetch('/dir_2').map(&:slug) - expect(dir_2_page_slugs).to eq(expected_ordered_page_slugs) + actual_order = + grouped_entries.map do |page_or_dir| + get_slugs(page_or_dir) + end. + flatten + expect(actual_order).to eq(expected_order) end end end @@ -336,4 +340,12 @@ describe WikiPage, models: true do page = wiki.wiki.paged(title) wiki.wiki.delete_page(page, commit_details) end + + def get_slugs(page_or_dir) + if page_or_dir.is_a? WikiPage + [page_or_dir.slug] + else + page_or_dir.pages.present? ? page_or_dir.pages.map(&:slug) : [] + end + end end From 84735186a8ae73a722715f286653ccd71e7e48e8 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Mon, 26 Dec 2016 23:51:34 -0200 Subject: [PATCH 028/488] Add WikiPage#to_partial_path --- app/models/wiki_page.rb | 6 ++++++ spec/models/wiki_page_spec.rb | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 63e5aa0e519..96d03d510ff 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -207,6 +207,12 @@ class WikiPage end end + # Relative path to the partial to be used when rendering collections + # of this object. + def to_partial_path + 'projects/wikis/wiki_page' + end + private def set_attributes diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 9eb94cb028d..11efd0415d9 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -322,6 +322,14 @@ describe WikiPage, models: true do end end + describe '#to_partial_path' do + it 'returns the relative path to the partial to be used' do + page = build(:wiki_page) + + expect(page.to_partial_path).to eq('projects/wikis/wiki_page') + end + end + private def remove_temp_repo(path) From 7bd68ae0799a982a4113de3480bef0d51ecb2f1c Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Mon, 26 Dec 2016 23:52:26 -0200 Subject: [PATCH 029/488] Add WikiDirectory#to_partial_path --- app/models/wiki_directory.rb | 6 ++++++ spec/models/wiki_directory_spec.rb | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/app/models/wiki_directory.rb b/app/models/wiki_directory.rb index c126a4d0421..561e5a497bc 100644 --- a/app/models/wiki_directory.rb +++ b/app/models/wiki_directory.rb @@ -10,4 +10,10 @@ class WikiDirectory @pages = pages @directories = directories end + + # Relative path to the partial to be used when rendering collections + # of this object. + def to_partial_path + 'projects/wikis/wiki_directory' + end end diff --git a/spec/models/wiki_directory_spec.rb b/spec/models/wiki_directory_spec.rb index 8362a285c54..fac70f8d3c7 100644 --- a/spec/models/wiki_directory_spec.rb +++ b/spec/models/wiki_directory_spec.rb @@ -42,4 +42,12 @@ RSpec.describe WikiDirectory, models: true do end end end + + describe '#to_partial_path' do + it 'returns the relative path to the partial to be used' do + directory = build(:wiki_directory) + + expect(directory.to_partial_path).to eq('projects/wikis/wiki_directory') + end + end end From a5625c749b31760daf104241475a9b3527eb223c Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Mon, 26 Dec 2016 23:54:36 -0200 Subject: [PATCH 030/488] Render wiki entries using a collection of WikiPage and WikiDirectory objects --- app/controllers/projects/wikis_controller.rb | 4 ++-- app/views/projects/wikis/_sidebar.html.haml | 10 +--------- app/views/projects/wikis/_sidebar_wiki_pages.html.haml | 4 ---- app/views/projects/wikis/_wiki_directory.html.haml | 4 ++++ .../{_wiki_pages.html.haml => _wiki_page.html.haml} | 6 +++++- app/views/projects/wikis/pages.html.haml | 9 +-------- 6 files changed, 13 insertions(+), 24 deletions(-) delete mode 100644 app/views/projects/wikis/_sidebar_wiki_pages.html.haml create mode 100644 app/views/projects/wikis/_wiki_directory.html.haml rename app/views/projects/wikis/{_wiki_pages.html.haml => _wiki_page.html.haml} (52%) diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 45a42400b2a..116c854b1ae 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -8,7 +8,7 @@ class Projects::WikisController < Projects::ApplicationController def pages @wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]) - @wiki_directories = WikiPage.group_by_directory(@wiki_pages) + @wiki_entries = WikiPage.group_by_directory(@wiki_pages) end def show @@ -117,7 +117,7 @@ class Projects::WikisController < Projects::ApplicationController # Call #wiki to make sure the Wiki Repo is initialized @project_wiki.wiki - @sidebar_wiki_directories = WikiPage.group_by_directory(@project_wiki.pages.first(15)) + @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15)) rescue ProjectWiki::CouldNotCreateWikiError flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." redirect_to project_path(@project) diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index b7464180a0c..e3fddfba689 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -12,15 +12,7 @@ .blocks-container .block.block-first %ul.wiki-pages - - @sidebar_wiki_directories.each do |wiki_directory, wiki_pages| - - if wiki_directory == '/' - = render 'sidebar_wiki_pages', wiki_pages: wiki_pages - - else - %li - = wiki_directory - %ul - = render 'sidebar_wiki_pages', wiki_pages: wiki_pages - + = render @sidebar_wiki_entries, context: 'sidebar' .block = link_to namespace_project_wikis_pages_path(@project.namespace, @project), class: 'btn btn-block' do diff --git a/app/views/projects/wikis/_sidebar_wiki_pages.html.haml b/app/views/projects/wikis/_sidebar_wiki_pages.html.haml deleted file mode 100644 index 65453a384d2..00000000000 --- a/app/views/projects/wikis/_sidebar_wiki_pages.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- wiki_pages.each do |wiki_page| - %li{ class: params[:id] == wiki_page.slug ? 'active' : '' } - = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do - = wiki_page.title.capitalize diff --git a/app/views/projects/wikis/_wiki_directory.html.haml b/app/views/projects/wikis/_wiki_directory.html.haml new file mode 100644 index 00000000000..0e5f32ed859 --- /dev/null +++ b/app/views/projects/wikis/_wiki_directory.html.haml @@ -0,0 +1,4 @@ +%li + = wiki_directory.slug + %ul + = render wiki_directory.pages, context: context diff --git a/app/views/projects/wikis/_wiki_pages.html.haml b/app/views/projects/wikis/_wiki_page.html.haml similarity index 52% rename from app/views/projects/wikis/_wiki_pages.html.haml rename to app/views/projects/wikis/_wiki_page.html.haml index ac98599d96b..cea27388a0d 100644 --- a/app/views/projects/wikis/_wiki_pages.html.haml +++ b/app/views/projects/wikis/_wiki_page.html.haml @@ -1,4 +1,8 @@ -- wiki_pages.each do |wiki_page| +- if context == 'sidebar' + %li{ class: params[:id] == wiki_page.slug ? 'active' : '' } + = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do + = wiki_page.title.capitalize +- else %li = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page) %small (#{wiki_page.format}) diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index 28dd81e5c3f..5fba2b1a5ae 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -14,13 +14,6 @@ Clone repository %ul.wiki-pages-list.content-list - - @wiki_directories.each do |wiki_directory, wiki_pages| - - if wiki_directory == '/' - = render 'wiki_pages', wiki_pages: wiki_pages - - else - %li - = wiki_directory - %ul - = render 'wiki_pages', wiki_pages: wiki_pages + = render @wiki_entries, context: 'pages' = paginate @wiki_pages, theme: 'gitlab' From 84cc7c3704cc0cc22a325572f35cd21d0e2a6cc7 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Tue, 27 Dec 2016 00:14:30 -0200 Subject: [PATCH 031/488] Stop rendering page full path at projects/wikis/show.html.haml --- app/assets/stylesheets/pages/wiki.scss | 1 - app/views/projects/wikis/show.html.haml | 1 - 2 files changed, 2 deletions(-) diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 1a22cd7d33f..369fb44d818 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -22,7 +22,6 @@ font-size: 22px; } - .wiki-page-full-path, .wiki-last-edit-by { display: block; color: $gl-gray-light; diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 25ae5c587ec..87b9ff6e415 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -8,7 +8,6 @@ .nav-text %h2.wiki-page-title= @page.title.capitalize - %span.wiki-page-full-path= "(#{@page.full_path})" %span.wiki-last-edit-by Last edited by %strong From 94dcadd62ac66cc5c52579ae9c288314bbca0c20 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Tue, 27 Dec 2016 01:44:03 -0200 Subject: [PATCH 032/488] Add a breadcrumb at projects/wikis/show.html.haml --- app/assets/stylesheets/pages/wiki.scss | 5 +++++ app/helpers/wiki_helper.rb | 13 +++++++++++++ app/views/projects/wikis/show.html.haml | 3 +++ spec/helpers/wiki_helper_spec.rb | 21 +++++++++++++++++++++ 4 files changed, 42 insertions(+) create mode 100644 app/helpers/wiki_helper.rb create mode 100644 spec/helpers/wiki_helper_spec.rb diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 369fb44d818..480cb2b9f0d 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -17,6 +17,11 @@ @extend .top-area; position: relative; + .wiki-breadcrumb { + border-bottom: 1px solid $white-normal; + padding: 11px 0; + } + .wiki-page-title { margin: 0; font-size: 22px; diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb new file mode 100644 index 00000000000..76ee632ab6d --- /dev/null +++ b/app/helpers/wiki_helper.rb @@ -0,0 +1,13 @@ +module WikiHelper + # Produces a pure text breadcrumb for a given page. + # + # page_slug - The slug of a WikiPage object. + # + # Returns a String composed of the capitalized name of each directory and the + # capitalized name of the page itself. + def breadcrumb(page_slug) + page_slug.split('/'). + map { |dir_or_page| dir_or_page.gsub(/-+/, ' ').capitalize }. + join(' / ') + end +end diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 87b9ff6e415..3609461b721 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -6,6 +6,9 @@ %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') + .wiki-breadcrumb + %span= breadcrumb(@page.slug) + .nav-text %h2.wiki-page-title= @page.title.capitalize %span.wiki-last-edit-by diff --git a/spec/helpers/wiki_helper_spec.rb b/spec/helpers/wiki_helper_spec.rb new file mode 100644 index 00000000000..92c6f27a867 --- /dev/null +++ b/spec/helpers/wiki_helper_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe WikiHelper do + describe '#breadcrumb' do + context 'when the page is at the root level' do + it 'returns the capitalized page name' do + slug = 'page-name' + + expect(helper.breadcrumb(slug)).to eq('Page name') + end + end + + context 'when the page is inside a directory' do + it 'returns the capitalized name of each directory and of the page itself' do + slug = 'dir_1/page-name' + + expect(helper.breadcrumb(slug)).to eq('Dir_1 / Page name') + end + end + end +end From 104bfa2a3187aefebd4a53be1ad14600dc7781e9 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Tue, 27 Dec 2016 01:52:50 -0200 Subject: [PATCH 033/488] Remove WikiPage#full_path --- app/models/wiki_page.rb | 5 ----- spec/models/wiki_page_spec.rb | 20 -------------------- 2 files changed, 25 deletions(-) diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 96d03d510ff..dec58681198 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -114,11 +114,6 @@ class WikiPage @attributes[:format] || :markdown end - # The full path for this page, including its filename and extension. - def full_path - "#{directory}/#{page.filename}".gsub(/\/+/, '/') - end - # The commit message for this page version. def message version.try(:message) diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 11efd0415d9..482f98e22f1 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -270,26 +270,6 @@ describe WikiPage, models: true do end end - describe '#full_path' do - context 'when the page is at the root directory' do - it 'returns /filename.fileextension' do - create_page('file', 'content') - page = wiki.find_page('file') - - expect(page.full_path).to eq('/file.md') - end - end - - context 'when the page is inside an actual directory' do - it 'returns /directory/filename.fileextension' do - create_page('dir/file', 'content') - page = wiki.find_page('dir/file') - - expect(page.full_path).to eq('/dir/file.md') - end - end - end - describe '#historical?' do before do create_page('Update', 'content') From d2b3fe45af8d458b935b3bbfc1558e21c1476d0a Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Tue, 27 Dec 2016 02:05:53 -0200 Subject: [PATCH 034/488] Change WikiPage#directory --- app/models/wiki_page.rb | 9 ++++----- spec/models/wiki_page_spec.rb | 6 +++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index dec58681198..6c237306eff 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -25,10 +25,10 @@ class WikiPage pages.sort_by { |page| [page.directory, page.slug] }. group_by { |page| page.directory }. map do |dir, pages| - if dir == '/' - pages - else + if dir.present? WikiDirectory.new(dir, pages) + else + pages end end. flatten @@ -98,8 +98,7 @@ class WikiPage # The hierarchy of the directory this page is contained in. def directory - dir = wiki.page_title_and_dir(slug).last - "/#{dir}" + wiki.page_title_and_dir(slug).last end # The processed/formatted content of this page. diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 482f98e22f1..109a0499090 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -252,11 +252,11 @@ describe WikiPage, models: true do describe '#directory' do context 'when the page is at the root directory' do - it 'returns /' do + it 'returns an empty string' do create_page('file', 'content') page = wiki.find_page('file') - expect(page.directory).to eq('/') + expect(page.directory).to eq('') end end @@ -265,7 +265,7 @@ describe WikiPage, models: true do create_page('dir_1/dir_1_1/file', 'content') page = wiki.find_page('dir_1/dir_1_1/file') - expect(page.directory).to eq('/dir_1/dir_1_1') + expect(page.directory).to eq('dir_1/dir_1_1') end end end From 389bd6b7356e78668831e4628f9ca8dadb01fcf2 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Sat, 31 Dec 2016 17:03:20 -0200 Subject: [PATCH 035/488] Improve WikiPage.group_by_directory --- app/models/wiki_page.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 6c237306eff..20bd9719b2f 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -23,7 +23,7 @@ class WikiPage return [] if pages.blank? pages.sort_by { |page| [page.directory, page.slug] }. - group_by { |page| page.directory }. + group_by(&:directory). map do |dir, pages| if dir.present? WikiDirectory.new(dir, pages) From b0ad4e0e87c642efefa840eeeea5824191e81405 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Sat, 31 Dec 2016 17:14:45 -0200 Subject: [PATCH 036/488] Add new wiki related partials --- app/views/projects/wikis/_pages_wiki_page.html.haml | 5 +++++ app/views/projects/wikis/_sidebar_wiki_page.html.haml | 3 +++ app/views/projects/wikis/_wiki_page.html.haml | 11 +---------- 3 files changed, 9 insertions(+), 10 deletions(-) create mode 100644 app/views/projects/wikis/_pages_wiki_page.html.haml create mode 100644 app/views/projects/wikis/_sidebar_wiki_page.html.haml diff --git a/app/views/projects/wikis/_pages_wiki_page.html.haml b/app/views/projects/wikis/_pages_wiki_page.html.haml new file mode 100644 index 00000000000..6298cf6c8da --- /dev/null +++ b/app/views/projects/wikis/_pages_wiki_page.html.haml @@ -0,0 +1,5 @@ +%li + = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page) + %small (#{wiki_page.format}) + .pull-right + %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)} diff --git a/app/views/projects/wikis/_sidebar_wiki_page.html.haml b/app/views/projects/wikis/_sidebar_wiki_page.html.haml new file mode 100644 index 00000000000..eb9bd14920d --- /dev/null +++ b/app/views/projects/wikis/_sidebar_wiki_page.html.haml @@ -0,0 +1,3 @@ +%li{ class: params[:id] == wiki_page.slug ? 'active' : '' } + = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do + = wiki_page.title.capitalize diff --git a/app/views/projects/wikis/_wiki_page.html.haml b/app/views/projects/wikis/_wiki_page.html.haml index cea27388a0d..c84d06dad02 100644 --- a/app/views/projects/wikis/_wiki_page.html.haml +++ b/app/views/projects/wikis/_wiki_page.html.haml @@ -1,10 +1 @@ -- if context == 'sidebar' - %li{ class: params[:id] == wiki_page.slug ? 'active' : '' } - = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do - = wiki_page.title.capitalize -- else - %li - = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page) - %small (#{wiki_page.format}) - .pull-right - %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)} += render "#{context}_wiki_page", wiki_page: wiki_page From 48417893d7456dc0d46b0a514a2326cc8ce6076f Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Sat, 31 Dec 2016 17:27:03 -0200 Subject: [PATCH 037/488] Remove directories as one of the attributes of WikiDirectory --- app/models/wiki_directory.rb | 5 ++--- spec/models/wiki_directory_spec.rb | 15 +++------------ 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/app/models/wiki_directory.rb b/app/models/wiki_directory.rb index 561e5a497bc..9340fc2dbbe 100644 --- a/app/models/wiki_directory.rb +++ b/app/models/wiki_directory.rb @@ -1,14 +1,13 @@ class WikiDirectory include ActiveModel::Validations - attr_accessor :slug, :pages, :directories + attr_accessor :slug, :pages validates :slug, presence: true - def initialize(slug, pages = [], directories = []) + def initialize(slug, pages = []) @slug = slug @pages = pages - @directories = directories end # Relative path to the partial to be used when rendering collections diff --git a/spec/models/wiki_directory_spec.rb b/spec/models/wiki_directory_spec.rb index fac70f8d3c7..1caaa557085 100644 --- a/spec/models/wiki_directory_spec.rb +++ b/spec/models/wiki_directory_spec.rb @@ -8,10 +8,9 @@ RSpec.describe WikiDirectory, models: true do end describe '#initialize' do - context 'when there are pages and directories' do + context 'when there are pages' do let(:pages) { [build(:wiki_page)] } - let(:other_directories) { [build(:wiki_directory)] } - let(:directory) { WikiDirectory.new('/path_up_to/dir', pages, other_directories) } + let(:directory) { WikiDirectory.new('/path_up_to/dir', pages) } it 'sets the slug attribute' do expect(directory.slug).to eq('/path_up_to/dir') @@ -20,13 +19,9 @@ RSpec.describe WikiDirectory, models: true do it 'sets the pages attribute' do expect(directory.pages).to eq(pages) end - - it 'sets the directories attribute' do - expect(directory.directories).to eq(other_directories) - end end - context 'when there are no pages or directories' do + context 'when there are no pages' do let(:directory) { WikiDirectory.new('/path_up_to/dir') } it 'sets the slug attribute' do @@ -36,10 +31,6 @@ RSpec.describe WikiDirectory, models: true do it 'sets the pages attribute to an empty array' do expect(directory.pages).to eq([]) end - - it 'sets the directories attribute to an empty array' do - expect(directory.directories).to eq([]) - end end end From d2d1b5c4b26d5740ca83a731495127eed4507994 Mon Sep 17 00:00:00 2001 From: Vitaly Baev Date: Mon, 9 Jan 2017 11:53:57 +0300 Subject: [PATCH 038/488] =?UTF-8?q?Disallow=20CI=E2=80=99s=20finished=20ti?= =?UTF-8?q?me=20wrapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Vitaly Baev --- app/assets/stylesheets/pages/pipelines.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index ed53ad94021..b0da1708947 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -213,6 +213,7 @@ .finished-at { color: $gl-text-color-secondary; margin: 4px 0; + white-space: nowrap; .fa { font-size: 12px; From 256a55d47be7838bbc061ddf9feb04b76e2a15ca Mon Sep 17 00:00:00 2001 From: the-undefined Date: Sat, 19 Nov 2016 06:12:47 +0000 Subject: [PATCH 039/488] Move 'User Snippets' Spinach feature to Rspec This commit moves the `snippets/user.feature` Spinach test to a Rspec feature, as part of deprecating the Spinach test suite. - Remove Spinach discover snippets feature and steps - Add Rspec feature test --- features/snippets/user.feature | 34 ------------ features/steps/snippets/user.rb | 55 -------------------- spec/features/snippets/user_snippets_spec.rb | 49 +++++++++++++++++ 3 files changed, 49 insertions(+), 89 deletions(-) delete mode 100644 features/snippets/user.feature delete mode 100644 features/steps/snippets/user.rb create mode 100644 spec/features/snippets/user_snippets_spec.rb diff --git a/features/snippets/user.feature b/features/snippets/user.feature deleted file mode 100644 index 5b5dadb7b39..00000000000 --- a/features/snippets/user.feature +++ /dev/null @@ -1,34 +0,0 @@ -@snippets -Feature: Snippets User - Background: - Given I sign in as a user - And I have public "Personal snippet one" snippet - And I have private "Personal snippet private" snippet - And I have internal "Personal snippet internal" snippet - - Scenario: I should see all my snippets - Given I visit my snippets page - Then I should see "Personal snippet one" in snippets - And I should see "Personal snippet private" in snippets - And I should see "Personal snippet internal" in snippets - - Scenario: I can see only my private snippets - Given I visit my snippets page - And I click "Private" filter - Then I should not see "Personal snippet one" in snippets - And I should not see "Personal snippet internal" in snippets - And I should see "Personal snippet private" in snippets - - Scenario: I can see only my public snippets - Given I visit my snippets page - And I click "Public" filter - Then I should see "Personal snippet one" in snippets - And I should not see "Personal snippet private" in snippets - And I should not see "Personal snippet internal" in snippets - - Scenario: I can see only my internal snippets - Given I visit my snippets page - And I click "Internal" filter - Then I should see "Personal snippet internal" in snippets - And I should not see "Personal snippet private" in snippets - And I should not see "Personal snippet one" in snippets diff --git a/features/steps/snippets/user.rb b/features/steps/snippets/user.rb deleted file mode 100644 index 997c605bce2..00000000000 --- a/features/steps/snippets/user.rb +++ /dev/null @@ -1,55 +0,0 @@ -class Spinach::Features::SnippetsUser < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedSnippet - - step 'I visit my snippets page' do - visit dashboard_snippets_path - end - - step 'I should see "Personal snippet one" in snippets' do - expect(page).to have_content "Personal snippet one" - end - - step 'I should see "Personal snippet private" in snippets' do - expect(page).to have_content "Personal snippet private" - end - - step 'I should see "Personal snippet internal" in snippets' do - expect(page).to have_content "Personal snippet internal" - end - - step 'I should not see "Personal snippet one" in snippets' do - expect(page).not_to have_content "Personal snippet one" - end - - step 'I should not see "Personal snippet private" in snippets' do - expect(page).not_to have_content "Personal snippet private" - end - - step 'I should not see "Personal snippet internal" in snippets' do - expect(page).not_to have_content "Personal snippet internal" - end - - step 'I click "Internal" filter' do - page.within('.snippet-scope-menu') do - click_link "Internal" - end - end - - step 'I click "Private" filter' do - page.within('.snippet-scope-menu') do - click_link "Private" - end - end - - step 'I click "Public" filter' do - page.within('.snippet-scope-menu') do - click_link "Public" - end - end - - def snippet - @snippet ||= PersonalSnippet.find_by!(title: "Personal snippet one") - end -end diff --git a/spec/features/snippets/user_snippets_spec.rb b/spec/features/snippets/user_snippets_spec.rb new file mode 100644 index 00000000000..191c2fb9a22 --- /dev/null +++ b/spec/features/snippets/user_snippets_spec.rb @@ -0,0 +1,49 @@ +require 'rails_helper' + +feature 'User Snippets', feature: true do + let(:author) { create(:user) } + let!(:public_snippet) { create(:personal_snippet, :public, author: author, title: "This is a public snippet") } + let!(:internal_snippet) { create(:personal_snippet, :internal, author: author, title: "This is an internal snippet") } + let!(:private_snippet) { create(:personal_snippet, :private, author: author, title: "This is a private snippet") } + + background do + login_as author + visit dashboard_snippets_path + end + + scenario 'View all of my snippets' do + expect(page).to have_content(public_snippet.title) + expect(page).to have_content(internal_snippet.title) + expect(page).to have_content(private_snippet.title) + end + + scenario 'View my public snippets' do + page.within('.snippet-scope-menu') do + click_link "Public" + end + + expect(page).to have_content(public_snippet.title) + expect(page).not_to have_content(internal_snippet.title) + expect(page).not_to have_content(private_snippet.title) + end + + scenario 'View my internal snippets' do + page.within('.snippet-scope-menu') do + click_link "Internal" + end + + expect(page).not_to have_content(public_snippet.title) + expect(page).to have_content(internal_snippet.title) + expect(page).not_to have_content(private_snippet.title) + end + + scenario 'View my private snippets' do + page.within('.snippet-scope-menu') do + click_link "Private" + end + + expect(page).not_to have_content(public_snippet.title) + expect(page).not_to have_content(internal_snippet.title) + expect(page).to have_content(private_snippet.title) + end +end From d0db72983e2170a61bcef26c99ceb3df313fd10a Mon Sep 17 00:00:00 2001 From: Dongqing Hu Date: Wed, 18 Jan 2017 17:05:09 +0800 Subject: [PATCH 040/488] use the current_user parameter in MergeRequest#issues_mentioned_but_not_closing --- app/models/merge_request.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 10251302db8..881a49d4ac2 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -576,7 +576,7 @@ class MergeRequest < ActiveRecord::Base ext = Gitlab::ReferenceExtractor.new(project, current_user) ext.analyze(description) - ext.issues - closes_issues + ext.issues - closes_issues(current_user) end def target_project_path From da81add825b0b7b4a3b4351f67fbab25a947922c Mon Sep 17 00:00:00 2001 From: Dongqing Hu Date: Thu, 19 Jan 2017 12:18:49 +0800 Subject: [PATCH 041/488] should pass in current_user from MergeRequestsHelper --- app/helpers/merge_requests_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 8c2c4e8833b..01f6ada8d76 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -64,11 +64,11 @@ module MergeRequestsHelper end def mr_closes_issues - @mr_closes_issues ||= @merge_request.closes_issues + @mr_closes_issues ||= @merge_request.closes_issues(current_user) end def mr_issues_mentioned_but_not_closing - @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing + @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing(current_user) end def mr_change_branches_path(merge_request) From d15b7db1216f220b9f5af7e777cf04712483cbdf Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 17 Jan 2017 14:50:49 -0500 Subject: [PATCH 042/488] Fix References header parser for Microsoft Exchange Microsoft Exchange would append a comma and another message id into the References header, therefore we'll need to fallback and parse the header by ourselves. Closes #26567 --- lib/gitlab/email/receiver.rb | 17 +++++++- lib/gitlab/incoming_email.rb | 9 ++-- ...and_key_inside_references_with_a_comma.eml | 42 +++++++++++++++++++ .../email/handler/create_note_handler_spec.rb | 6 +++ spec/lib/gitlab/incoming_email_spec.rb | 15 +++++++ 5 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index a40c44eb1bc..df9d1cae8da 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -35,6 +35,8 @@ module Gitlab handler.execute end + private + def build_mail Mail::Message.new(@raw) rescue Encoding::UndefinedConversionError, @@ -54,7 +56,20 @@ module Gitlab end def key_from_additional_headers(mail) - Array(mail.references).find do |mail_id| + find_key_from_references(ensure_references_array(mail.references)) + end + + def ensure_references_array(references) + case references + when Array + references + when String # Handle emails from Microsoft exchange which uses commas + Gitlab::IncomingEmail.scan_fallback_references(references) + end + end + + def find_key_from_references(references) + references.find do |mail_id| key = Gitlab::IncomingEmail.key_from_fallback_message_id(mail_id) break key if key end diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb index 801dfde9a36..9ae3a2c1214 100644 --- a/lib/gitlab/incoming_email.rb +++ b/lib/gitlab/incoming_email.rb @@ -3,8 +3,6 @@ module Gitlab WILDCARD_PLACEHOLDER = '%{key}'.freeze class << self - FALLBACK_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze - def enabled? config.enabled && config.address end @@ -32,10 +30,11 @@ module Gitlab end def key_from_fallback_message_id(mail_id) - match = mail_id.match(FALLBACK_MESSAGE_ID_REGEX) - return unless match + mail_id[/\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/, 1] + end - match[1] + def scan_fallback_references(references) + references.scan(/(?!<)[^<>]+(?=>)/.freeze) end def config diff --git a/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml b/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml new file mode 100644 index 00000000000..6823db0cfc8 --- /dev/null +++ b/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml @@ -0,0 +1,42 @@ +Return-Path: +Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for ; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for ; Thu, 13 Jun 2013 14:03:48 -0700 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +Date: Thu, 13 Jun 2013 17:03:48 -0400 +From: Jake the Dog +To: reply@appmail.adventuretime.ooo +Message-ID: +In-Reply-To: +References: , +Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux' +Mime-Version: 1.0 +Content-Type: text/plain; + charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +X-Sieve: CMU Sieve 2.2 +X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, + 13 Jun 2013 14:03:48 -0700 (PDT) +X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 + +I could not disagree more. I am obviously biased but adventure time is the +greatest show ever created. Everyone should watch it. + +- Jake out + + +On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta + wrote: +> +> +> +> eviltrout posted in 'Adventure Time Sux' on Discourse Meta: +> +> --- +> hey guys everyone knows adventure time sucks! +> +> --- +> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3 +> +> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences). +> diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb index 48660d1dd1b..0f2bd009148 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -174,6 +174,12 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do it_behaves_like 'an email that contains a mail key', 'References' end + + context 'mail key is in the References header with a comma' do + let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml') } + + it_behaves_like 'an email that contains a mail key', 'References' + end end end end diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb index 1dcf2c0668b..01d0cb6cbd6 100644 --- a/spec/lib/gitlab/incoming_email_spec.rb +++ b/spec/lib/gitlab/incoming_email_spec.rb @@ -48,4 +48,19 @@ describe Gitlab::IncomingEmail, lib: true do expect(described_class.key_from_fallback_message_id('reply-key@localhost')).to eq('key') end end + + context 'self.scan_fallback_references' do + let(:references) do + '' + + ' ' + + ',' + end + + it 'returns reply key' do + expect(described_class.scan_fallback_references(references)) + .to eq(%w[issue_1@localhost + reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost + exchange@microsoft.com]) + end + end end From 7fcbe37df37cb9f04eae4e690305a26ea88410d2 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 20 Jan 2017 20:20:40 +0800 Subject: [PATCH 043/488] Specify that iOS app would also do this --- lib/gitlab/email/receiver.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index df9d1cae8da..fa08b5c668f 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -63,7 +63,9 @@ module Gitlab case references when Array references - when String # Handle emails from Microsoft exchange which uses commas + when String + # Handle emails from clients which append with commas, + # example clients are Microsoft exchange and iOS app Gitlab::IncomingEmail.scan_fallback_references(references) end end From 15f8642994bc74bea1a39d079c70b1f4e4730bf1 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 20 Jan 2017 22:36:12 +0800 Subject: [PATCH 044/488] Add changelog entry --- changelogs/unreleased/fix-references-header-parsing.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/fix-references-header-parsing.yml diff --git a/changelogs/unreleased/fix-references-header-parsing.yml b/changelogs/unreleased/fix-references-header-parsing.yml new file mode 100644 index 00000000000..b927279cdf4 --- /dev/null +++ b/changelogs/unreleased/fix-references-header-parsing.yml @@ -0,0 +1,5 @@ +--- +title: Fix reply by email without sub-addressing for some clients from + Microsoft and Apple +merge_request: 8620 +author: From 821ab7cf7dc45932167986005013044e346c8823 Mon Sep 17 00:00:00 2001 From: Dongqing Hu Date: Wed, 25 Jan 2017 10:44:05 +0800 Subject: [PATCH 045/488] tests for #mr_closes_issues and #mr_issues_mentioned_but_not_closing in MergeRequestsHelper --- ...n MergeRequest and MergeRequestsHelper.yml | 4 + spec/helpers/merge_requests_helper_spec.rb | 82 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 changelogs/unreleased/pass in current_user in MergeRequest and MergeRequestsHelper.yml diff --git a/changelogs/unreleased/pass in current_user in MergeRequest and MergeRequestsHelper.yml b/changelogs/unreleased/pass in current_user in MergeRequest and MergeRequestsHelper.yml new file mode 100644 index 00000000000..0751047c3c0 --- /dev/null +++ b/changelogs/unreleased/pass in current_user in MergeRequest and MergeRequestsHelper.yml @@ -0,0 +1,4 @@ +--- +title: pass in current_user in MergeRequest and MergeRequestsHelper +merge_request: 8624 +author: Dongqing Hu diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index 1f221487393..408ee93f7df 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -77,4 +77,86 @@ describe MergeRequestsHelper do expect(mr_widget_refresh_url(nil)).to end_with('') end end + + describe '#mr_closes_issues' do + let(:user_1) { create(:user) } + let(:user_2) { create(:user) } + + let(:project_1) { create(:project, :private, creator: user_1, namespace: user_1.namespace) } + let(:project_2) { create(:project, :private, creator: user_2, namespace: user_2.namespace) } + + let(:issue_1) { create(:issue, project: project_1) } + let(:issue_2) { create(:issue, project: project_2) } + + let(:merge_request) { create(:merge_request, source_project: project_1, target_project: project_1,) } + + let(:merge_request) do + create(:merge_request, + source_project: project_1, target_project: project_1, + description: "Fixes #{issue_1.to_reference} Fixes #{issue_2.to_reference(project_1)}") + end + + before do + project_1.team << [user_2, :developer] + project_2.team << [user_2, :developer] + allow(merge_request.project).to receive(:default_branch).and_return(merge_request.target_branch) + @merge_request = merge_request + end + + context 'user without access to another private project' do + let(:current_user) { user_1 } + + it 'cannot see that project\'s issue that will be closed on acceptance' do + expect(mr_closes_issues).to contain_exactly(issue_1) + end + end + + context 'user with access to another private project' do + let(:current_user) { user_2 } + + it 'can see that project\'s issue that will be closed on acceptance' do + expect(mr_closes_issues).to contain_exactly(issue_1, issue_2) + end + end + end + + describe '#mr_issues_mentioned_but_not_closing' do + let(:user_1) { create(:user) } + let(:user_2) { create(:user) } + + let(:project_1) { create(:project, :private, creator: user_1, namespace: user_1.namespace) } + let(:project_2) { create(:project, :private, creator: user_2, namespace: user_2.namespace) } + + let(:issue_1) { create(:issue, project: project_1) } + let(:issue_2) { create(:issue, project: project_2) } + + let(:merge_request) do + create(:merge_request, + source_project: project_1, target_project: project_1, + description: "#{issue_1.to_reference} #{issue_2.to_reference(project_1)}") + end + + before do + project_1.team << [user_2, :developer] + project_2.team << [user_2, :developer] + allow(merge_request.project).to receive(:default_branch).and_return(merge_request.target_branch) + @merge_request = merge_request + end + + context 'user without access to another private project' do + let(:current_user) { user_1 } + + it 'cannot see that project\'s issue that will be closed on acceptance' do + expect(mr_issues_mentioned_but_not_closing).to contain_exactly(issue_1) + end + end + + context 'user with access to another private project' do + let(:current_user) { user_2 } + + it 'can see that project\'s issue that will be closed on acceptance' do + expect(mr_issues_mentioned_but_not_closing).to contain_exactly(issue_1, issue_2) + end + end + end end From 860c8cfc35956adba73c38b9b21be5c58069ba81 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 25 Jan 2017 09:29:22 +0000 Subject: [PATCH 046/488] Added header to protected branches access dropdowns CE part of https://gitlab.com/gitlab-org/gitlab-ee/issues/1294 --- .../projects/protected_branches_controller.rb | 8 ++++++-- .../unreleased/protected-branch-dropdown-titles.yml | 4 ++++ .../protected_branches/access_control_ce_spec.rb | 12 ++++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/protected-branch-dropdown-titles.yml diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index 9a438d5512c..2f422d352ed 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -68,8 +68,12 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController def access_levels_options { - push_access_levels: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } }, - merge_access_levels: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } } + push_access_levels: { + "Roles" => ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } }, + }, + merge_access_levels: { + "Roles" => ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } } + } } end diff --git a/changelogs/unreleased/protected-branch-dropdown-titles.yml b/changelogs/unreleased/protected-branch-dropdown-titles.yml new file mode 100644 index 00000000000..df82cc00fc9 --- /dev/null +++ b/changelogs/unreleased/protected-branch-dropdown-titles.yml @@ -0,0 +1,4 @@ +--- +title: Added headers to protected branch access dropdowns +merge_request: +author: diff --git a/spec/features/protected_branches/access_control_ce_spec.rb b/spec/features/protected_branches/access_control_ce_spec.rb index 395c61a4743..e4aca25a339 100644 --- a/spec/features/protected_branches/access_control_ce_spec.rb +++ b/spec/features/protected_branches/access_control_ce_spec.rb @@ -26,7 +26,11 @@ RSpec.shared_examples "protected branches > access control > CE" do within(".protected-branches-list") do find(".js-allowed-to-push").click - within('.js-allowed-to-push-container') { click_on access_type_name } + + within('.js-allowed-to-push-container') do + expect(first("li")).to have_content("Roles") + click_on access_type_name + end end wait_for_ajax @@ -61,7 +65,11 @@ RSpec.shared_examples "protected branches > access control > CE" do within(".protected-branches-list") do find(".js-allowed-to-merge").click - within('.js-allowed-to-merge-container') { click_on access_type_name } + + within('.js-allowed-to-merge-container') do + expect(first("li")).to have_content("Roles") + click_on access_type_name + end end wait_for_ajax From c34e85edc1b0bd34bd4d20e1c94809d2adbc2169 Mon Sep 17 00:00:00 2001 From: Carlos Galarza Date: Sat, 28 Jan 2017 16:53:12 +0000 Subject: [PATCH 047/488] Fix link in CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d404f1b91df..e0d81ae8e20 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -393,7 +393,7 @@ merge request: 1. [Newlines styleguide][newlines-styleguide] 1. [Testing](doc/development/testing.md) 1. [JavaScript (ES6)](https://github.com/airbnb/javascript) -1. [JavaScript (ES5)](https://github.com/airbnb/javascript/tree/master/es5) +1. [JavaScript (ES5)](https://github.com/airbnb/javascript/tree/es5-deprecated/es5) 1. [SCSS styleguide][scss-styleguide] 1. [Shell commands](doc/development/shell_commands.md) created by GitLab contributors to enhance security From e4029070a205351ca9677311c9675f4f933e8f45 Mon Sep 17 00:00:00 2001 From: Jacopo Date: Sat, 28 Jan 2017 23:39:24 +0100 Subject: [PATCH 048/488] Filter todos by manual add Added the option to filter todo by Added and Pipelines --- app/helpers/todos_helper.rb | 4 +- .../26705-filter-todos-by-manual-add.yml | 4 ++ spec/factories/todos.rb | 4 ++ spec/features/todos/todos_filtering_spec.rb | 57 ++++++++++++++++--- 4 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 changelogs/unreleased/26705-filter-todos-by-manual-add.yml diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index c568cca9e5e..d7d51c99979 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -86,7 +86,9 @@ module TodosHelper [ { id: '', text: 'Any Action' }, { id: Todo::ASSIGNED, text: 'Assigned' }, - { id: Todo::MENTIONED, text: 'Mentioned' } + { id: Todo::MENTIONED, text: 'Mentioned' }, + { id: Todo::MARKED, text: 'Added' }, + { id: Todo::BUILD_FAILED, text: 'Pipelines' } ] end diff --git a/changelogs/unreleased/26705-filter-todos-by-manual-add.yml b/changelogs/unreleased/26705-filter-todos-by-manual-add.yml new file mode 100644 index 00000000000..3521496a20e --- /dev/null +++ b/changelogs/unreleased/26705-filter-todos-by-manual-add.yml @@ -0,0 +1,4 @@ +--- +title: Filter todos by manual add +merge_request: 8691 +author: Jacopo Beschi @jacopo-beschi diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index 91d6f39a5bf..275561502cd 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -24,6 +24,10 @@ FactoryGirl.define do target factory: :merge_request end + trait :marked do + action { Todo::MARKED } + end + trait :approval_required do action { Todo::APPROVAL_REQUIRED } end diff --git a/spec/features/todos/todos_filtering_spec.rb b/spec/features/todos/todos_filtering_spec.rb index d1f2bc78884..e8f06916d53 100644 --- a/spec/features/todos/todos_filtering_spec.rb +++ b/spec/features/todos/todos_filtering_spec.rb @@ -98,15 +98,58 @@ describe 'Dashboard > User filters todos', feature: true, js: true do expect(find('.todos-list')).not_to have_content merge_request.to_reference end - it 'filters by action' do - click_button 'Action' - within '.dropdown-menu-action' do - click_link 'Assigned' + describe 'filter by action' do + before do + create(:todo, :build_failed, user: user_1, author: user_2, project: project_1) + create(:todo, :marked, user: user_1, author: user_2, project: project_1, target: issue) end - wait_for_ajax + it 'filters by Assigned' do + filter_action('Assigned') - expect(find('.todos-list')).to have_content ' assigned you ' - expect(find('.todos-list')).not_to have_content ' mentioned ' + expect_to_see_action(:assigned) + end + + it 'filters by Mentioned' do + filter_action('Mentioned') + + expect_to_see_action(:mentioned) + end + + it 'filters by Added' do + filter_action('Added') + + expect_to_see_action(:marked) + end + + it 'filters by Pipelines' do + filter_action('Pipelines') + + expect_to_see_action(:build_failed) + end + + def filter_action(name) + click_button 'Action' + within '.dropdown-menu-action' do + click_link name + end + + wait_for_ajax + end + + def expect_to_see_action(action_name) + action_names = { + assigned: ' assigned you ', + mentioned: ' mentioned ', + marked: ' added a todo for ', + build_failed: ' build failed for ' + } + + action_name_text = action_names.delete(action_name) + expect(find('.todos-list')).to have_content action_name_text + action_names.each_value do |other_action_text| + expect(find('.todos-list')).not_to have_content other_action_text + end + end end end From 683097666aa01ef6a5b490be67a4a0d9733152e3 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Mon, 30 Jan 2017 01:07:31 -0200 Subject: [PATCH 049/488] Add WikiPage.unhyphenize --- app/helpers/wiki_helper.rb | 2 +- app/models/wiki_page.rb | 6 +++++- spec/models/wiki_page_spec.rb | 8 ++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb index 76ee632ab6d..3e3f6246fc5 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -7,7 +7,7 @@ module WikiHelper # capitalized name of the page itself. def breadcrumb(page_slug) page_slug.split('/'). - map { |dir_or_page| dir_or_page.gsub(/-+/, ' ').capitalize }. + map { |dir_or_page| WikiPage.unhyphenize(dir_or_page).capitalize }. join(' / ') end end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 20bd9719b2f..2f4f92846b4 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -34,6 +34,10 @@ class WikiPage flatten end + def self.unhyphenize(name) + name.gsub(/-+/, ' ') + end + def to_key [:slug] end @@ -78,7 +82,7 @@ class WikiPage # The formatted title of this page. def title if @attributes[:title] - @attributes[:title].gsub(/-+/, ' ') + self.class.unhyphenize(@attributes[:title]) else "" end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 109a0499090..579ebac7afb 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -68,6 +68,14 @@ describe WikiPage, models: true do end end + describe '.unhyphenize' do + it 'removes hyphens from a name' do + name = 'a-name--with-hyphens' + + expect(WikiPage.unhyphenize(name)).to eq('a name with hyphens') + end + end + describe "#initialize" do context "when initialized with an existing gollum page" do before do From 89347fb688820667ce55089daa600277796871a5 Mon Sep 17 00:00:00 2001 From: Alex Braha Stoll Date: Mon, 30 Jan 2017 01:08:36 -0200 Subject: [PATCH 050/488] Add merge request number --- changelogs/unreleased/23535-folders-in-wiki-repository.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/unreleased/23535-folders-in-wiki-repository.yml b/changelogs/unreleased/23535-folders-in-wiki-repository.yml index 7361b182a94..05212b608d4 100644 --- a/changelogs/unreleased/23535-folders-in-wiki-repository.yml +++ b/changelogs/unreleased/23535-folders-in-wiki-repository.yml @@ -1,4 +1,4 @@ --- title: Show directory hierarchy when listing wiki pages -merge_request: +merge_request: 8133 author: Alex Braha Stoll From bbbef273f74a59a18cf534e147e79e90888d7656 Mon Sep 17 00:00:00 2001 From: Dongqing Hu Date: Tue, 31 Jan 2017 22:42:54 +0800 Subject: [PATCH 051/488] Remove MergeRequest#closes_issue?; Remove the default parameter value for #cache_merge_request_closes_issues! and #issues_mentioned_but_not_closing --- app/models/merge_request.rb | 8 ++------ spec/models/merge_request_spec.rb | 6 +++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 881a49d4ac2..9e5e5a3b70a 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -541,7 +541,7 @@ class MergeRequest < ActiveRecord::Base # Calculating this information for a number of merge requests requires # running `ReferenceExtractor` on each of them separately. # This optimization does not apply to issues from external sources. - def cache_merge_request_closes_issues!(current_user = self.author) + def cache_merge_request_closes_issues!(current_user) return if project.has_external_issue_tracker? transaction do @@ -553,10 +553,6 @@ class MergeRequest < ActiveRecord::Base end end - def closes_issue?(issue) - closes_issues.include?(issue) - end - # Return the set of issues that will be closed if this merge request is accepted. def closes_issues(current_user = self.author) if target_branch == project.default_branch @@ -570,7 +566,7 @@ class MergeRequest < ActiveRecord::Base end end - def issues_mentioned_but_not_closing(current_user = self.author) + def issues_mentioned_but_not_closing(current_user) return [] unless target_branch == project.default_branch ext = Gitlab::ReferenceExtractor.new(project, current_user) diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 861426acbc3..cfb5ff0ff8b 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -97,7 +97,7 @@ describe MergeRequest, models: true do commit = double('commit1', safe_message: "Fixes #{issue.to_reference}") allow(subject).to receive(:commits).and_return([commit]) - expect { subject.cache_merge_request_closes_issues! }.to change(subject.merge_requests_closing_issues, :count).by(1) + expect { subject.cache_merge_request_closes_issues!(subject.author) }.to change(subject.merge_requests_closing_issues, :count).by(1) end it 'does not cache issues from external trackers' do @@ -106,7 +106,7 @@ describe MergeRequest, models: true do commit = double('commit1', safe_message: "Fixes #{issue.to_reference}") allow(subject).to receive(:commits).and_return([commit]) - expect { subject.cache_merge_request_closes_issues! }.not_to change(subject.merge_requests_closing_issues, :count) + expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count) end end @@ -300,7 +300,7 @@ describe MergeRequest, models: true do allow(subject.project).to receive(:default_branch). and_return(subject.target_branch) - expect(subject.issues_mentioned_but_not_closing).to match_array([mentioned_issue]) + expect(subject.issues_mentioned_but_not_closing(subject.author)).to match_array([mentioned_issue]) end end From 67cec150cc5a991846a45dffdd699efbb1b65187 Mon Sep 17 00:00:00 2001 From: Richard Macklin Date: Fri, 27 Jan 2017 01:31:42 -0800 Subject: [PATCH 052/488] Add controller spec for Profiles::NotificationsController --- .../profiles/notifications_controller_spec.rb | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 spec/controllers/profiles/notifications_controller_spec.rb diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb new file mode 100644 index 00000000000..55acc445e43 --- /dev/null +++ b/spec/controllers/profiles/notifications_controller_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe Profiles::NotificationsController do + describe 'GET show' do + it 'renders' do + user = create_user + sign_in(user) + + get :show + expect(response).to render_template :show + end + end + + describe 'POST update' do + it 'updates only permitted attributes' do + user = create_user + sign_in(user) + + put :update, user: { notification_email: 'new@example.com', admin: true } + + user.reload + expect(user.notification_email).to eq('new@example.com') + expect(user.admin).to eq(false) + expect(controller).to set_flash[:notice].to('Notification settings saved') + end + + it 'shows an error message if the params are invalid' do + user = create_user + sign_in(user) + + put :update, user: { notification_email: '' } + + expect(user.reload.notification_email).to eq('original@example.com') + expect(controller).to set_flash[:alert].to('Failed to save new settings') + end + end + + def create_user + create(:user) do |user| + user.emails.create(email: 'original@example.com') + user.emails.create(email: 'new@example.com') + user.update(notification_email: 'original@example.com') + user.save! + end + end +end From bd03ca4a8e5b041f84db85f9043aaefd669afb82 Mon Sep 17 00:00:00 2001 From: Richard Macklin Date: Fri, 27 Jan 2017 01:31:49 -0800 Subject: [PATCH 053/488] Add notified_of_own_activity column to users table --- ...061730_add_notified_of_own_activity_to_users.rb | 14 ++++++++++++++ db/schema.rb | 1 + 2 files changed, 15 insertions(+) create mode 100644 db/migrate/20170123061730_add_notified_of_own_activity_to_users.rb diff --git a/db/migrate/20170123061730_add_notified_of_own_activity_to_users.rb b/db/migrate/20170123061730_add_notified_of_own_activity_to_users.rb new file mode 100644 index 00000000000..f90637e1e35 --- /dev/null +++ b/db/migrate/20170123061730_add_notified_of_own_activity_to_users.rb @@ -0,0 +1,14 @@ +class AddNotifiedOfOwnActivityToUsers < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_column_with_default :users, :notified_of_own_activity, :boolean, default: false + end + + def down + remove_column :users, :notified_of_own_activity + end +end diff --git a/db/schema.rb b/db/schema.rb index 5efb4f6595c..4ef8d4bbe10 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1257,6 +1257,7 @@ ActiveRecord::Schema.define(version: 20170130204620) do t.string "organization" t.string "incoming_email_token" t.boolean "authorized_projects_populated" + t.boolean "notified_of_own_activity", default: false, null: false end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree From 0a0207ea91fdbe869ac70c23178b876bcbeb3021 Mon Sep 17 00:00:00 2001 From: Richard Macklin Date: Fri, 27 Jan 2017 01:31:53 -0800 Subject: [PATCH 054/488] Add notified_of_own_activity to permitted attributes in Profiles::NotificationsController#update --- app/controllers/profiles/notifications_controller.rb | 2 +- spec/controllers/profiles/notifications_controller_spec.rb | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index b8b71d295f6..a271e2dfc4b 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -17,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController end def user_params - params.require(:user).permit(:notification_email) + params.require(:user).permit(:notification_email, :notified_of_own_activity) end end diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb index 55acc445e43..54324cece6c 100644 --- a/spec/controllers/profiles/notifications_controller_spec.rb +++ b/spec/controllers/profiles/notifications_controller_spec.rb @@ -16,10 +16,11 @@ describe Profiles::NotificationsController do user = create_user sign_in(user) - put :update, user: { notification_email: 'new@example.com', admin: true } + put :update, user: { notification_email: 'new@example.com', notified_of_own_activity: true, admin: true } user.reload expect(user.notification_email).to eq('new@example.com') + expect(user.notified_of_own_activity).to eq(true) expect(user.admin).to eq(false) expect(controller).to set_flash[:notice].to('Notification settings saved') end From 3e81bc7b1daec9dfda602165d7e36cf5b6a39e20 Mon Sep 17 00:00:00 2001 From: Richard Macklin Date: Fri, 27 Jan 2017 01:31:57 -0800 Subject: [PATCH 055/488] Update NotificationService to respect User#notified_of_own_activity --- app/services/notification_service.rb | 6 +-- spec/services/notification_service_spec.rb | 59 ++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index b2cc39763f3..5a7d5ef8747 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -217,7 +217,7 @@ class NotificationService recipients = reject_unsubscribed_users(recipients, note.noteable) recipients = reject_users_without_access(recipients, note.noteable) - recipients.delete(note.author) + recipients.delete(note.author) unless note.author.notified_of_own_activity? recipients = recipients.uniq notify_method = "note_#{note.to_ability_name}_email".to_sym @@ -627,7 +627,7 @@ class NotificationService recipients = reject_unsubscribed_users(recipients, target) recipients = reject_users_without_access(recipients, target) - recipients.delete(current_user) if skip_current_user + recipients.delete(current_user) if skip_current_user && !current_user.try(:notified_of_own_activity?) recipients.uniq end @@ -636,7 +636,7 @@ class NotificationService recipients = add_labels_subscribers([], project, target, labels: labels) recipients = reject_unsubscribed_users(recipients, target) recipients = reject_users_without_access(recipients, target) - recipients.delete(current_user) + recipients.delete(current_user) unless current_user.notified_of_own_activity? recipients.uniq end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 7cf2cd9968f..839250b7d84 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -146,6 +146,16 @@ describe NotificationService, services: true do should_not_email(@u_lazy_participant) end + it "emails the note author if they've opted into notifications about their activity" do + add_users_with_subscription(note.project, issue) + note.author.notified_of_own_activity = true + reset_delivered_emails! + + notification.new_note(note) + + should_email(note.author) + end + it 'filters out "mentioned in" notes' do mentioned_note = SystemNoteService.cross_reference(mentioned_issue, issue, issue.author) @@ -476,6 +486,20 @@ describe NotificationService, services: true do should_not_email(issue.assignee) end + it "emails the author if they've opted into notifications about their activity" do + issue.author.notified_of_own_activity = true + + notification.new_issue(issue, issue.author) + + should_email(issue.author) + end + + it "doesn't email the author if they haven't opted into notifications about their activity" do + notification.new_issue(issue, issue.author) + + should_not_email(issue.author) + end + it "emails subscribers of the issue's labels" do user_1 = create(:user) user_2 = create(:user) @@ -665,6 +689,19 @@ describe NotificationService, services: true do should_email(subscriber_to_label_2) end + it "emails the current user if they've opted into notifications about their activity" do + subscriber_to_label_2.notified_of_own_activity = true + notification.relabeled_issue(issue, [group_label_2, label_2], subscriber_to_label_2) + + should_email(subscriber_to_label_2) + end + + it "doesn't email the current user if they haven't opted into notifications about their activity" do + notification.relabeled_issue(issue, [group_label_2, label_2], subscriber_to_label_2) + + should_not_email(subscriber_to_label_2) + end + it "doesn't send email to anyone but subscribers of the given labels" do notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled) @@ -818,6 +855,20 @@ describe NotificationService, services: true do should_not_email(@u_lazy_participant) end + it "emails the author if they've opted into notifications about their activity" do + merge_request.author.notified_of_own_activity = true + + notification.new_merge_request(merge_request, merge_request.author) + + should_email(merge_request.author) + end + + it "doesn't email the author if they haven't opted into notifications about their activity" do + notification.new_merge_request(merge_request, merge_request.author) + + should_not_email(merge_request.author) + end + it "emails subscribers of the merge request's labels" do user_1 = create(:user) user_2 = create(:user) @@ -1013,6 +1064,14 @@ describe NotificationService, services: true do should_not_email(@u_watcher) end + it "notifies the merger when merge_when_build_succeeds is false but they've opted into notifications about their activity" do + merge_request.merge_when_build_succeeds = false + @u_watcher.notified_of_own_activity = true + notification.merge_mr(merge_request, @u_watcher) + + should_email(@u_watcher) + end + it_behaves_like 'participating notifications' do let(:participant) { create(:user, username: 'user-participant') } let(:issuable) { merge_request } From 530d0fda7b97a9a3d8836a36b02e50bc5d408464 Mon Sep 17 00:00:00 2001 From: Richard Macklin Date: Fri, 27 Jan 2017 01:32:00 -0800 Subject: [PATCH 056/488] Add checkbox in UI to opt into receiving notifications about your activity --- app/assets/javascripts/profile/profile.js.es6 | 1 + .../profiles/notifications/show.html.haml | 5 +++ ...r_changes_notified_of_own_activity_spec.rb | 32 +++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 spec/features/profiles/user_changes_notified_of_own_activity_spec.rb diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6 index 5aec9c813fe..81374296522 100644 --- a/app/assets/javascripts/profile/profile.js.es6 +++ b/app/assets/javascripts/profile/profile.js.es6 @@ -25,6 +25,7 @@ bindEvents() { $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); $('#user_notification_email').on('change', this.submitForm); + $('#user_notified_of_own_activity').on('change', this.submitForm); $('.update-username').on('ajax:before', this.beforeUpdateUsername); $('.update-username').on('ajax:complete', this.afterUpdateUsername); $('.update-notifications').on('ajax:success', this.onUpdateNotifs); diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 5c5e5940365..51c4e8e5a73 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -34,6 +34,11 @@ .clearfix + = form_for @user, url: profile_notifications_path, method: :put do |f| + %label{ for: 'user_notified_of_own_activity' } + = f.check_box :notified_of_own_activity + %span Receive notifications about your own activity + %hr %h5 Groups (#{@group_notifications.count}) diff --git a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb new file mode 100644 index 00000000000..0709f32bf0c --- /dev/null +++ b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +feature 'Profile > Notifications > User changes notified_of_own_activity setting', feature: true, js: true do + let(:user) { create(:user) } + + before do + login_as(user) + end + + scenario 'User opts into receiving notifications about their own activity' do + visit profile_notifications_path + + expect(page).not_to have_checked_field('user[notified_of_own_activity]') + + page.find('#user_notified_of_own_activity').set(true) + + expect(page).to have_content('Notification settings saved') + expect(page).to have_checked_field('user[notified_of_own_activity]') + end + + scenario 'User opts out of receiving notifications about their own activity' do + user.update!(notified_of_own_activity: true) + visit profile_notifications_path + + expect(page).to have_checked_field('user[notified_of_own_activity]') + + page.find('#user_notified_of_own_activity').set(false) + + expect(page).to have_content('Notification settings saved') + expect(page).not_to have_checked_field('user[notified_of_own_activity]') + end +end From 2f17a583934a68eafb87cdabcb4ac3d53135c7ec Mon Sep 17 00:00:00 2001 From: Richard Macklin Date: Fri, 27 Jan 2017 02:02:50 -0800 Subject: [PATCH 057/488] Add changelog entry for option to be notified of your own activity --- .../unreleased/option-to-be-notified-of-own-activity.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/option-to-be-notified-of-own-activity.yml diff --git a/changelogs/unreleased/option-to-be-notified-of-own-activity.yml b/changelogs/unreleased/option-to-be-notified-of-own-activity.yml new file mode 100644 index 00000000000..c2e0410cc33 --- /dev/null +++ b/changelogs/unreleased/option-to-be-notified-of-own-activity.yml @@ -0,0 +1,4 @@ +--- +title: Add option to receive email notifications about your own activity +merge_request: 8836 +author: Richard Macklin From 946efd9fa690de68c6766cba063ff078af8699e1 Mon Sep 17 00:00:00 2001 From: Richard Macklin Date: Tue, 31 Jan 2017 18:27:02 -0800 Subject: [PATCH 058/488] Add missing newline in Profiles::NotificationsController spec --- spec/controllers/profiles/notifications_controller_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb index 54324cece6c..c056ba852f0 100644 --- a/spec/controllers/profiles/notifications_controller_spec.rb +++ b/spec/controllers/profiles/notifications_controller_spec.rb @@ -7,6 +7,7 @@ describe Profiles::NotificationsController do sign_in(user) get :show + expect(response).to render_template :show end end From 4647d13893d84dea5d0863c48a933dcc8a1ba679 Mon Sep 17 00:00:00 2001 From: Richard Macklin Date: Tue, 31 Jan 2017 18:27:14 -0800 Subject: [PATCH 059/488] Use check and uncheck methods from Capybara DSL in user_changes_notified_of_own_activity_spec --- .../profiles/user_changes_notified_of_own_activity_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb index 0709f32bf0c..e05fbb3715c 100644 --- a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb +++ b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb @@ -12,7 +12,7 @@ feature 'Profile > Notifications > User changes notified_of_own_activity setting expect(page).not_to have_checked_field('user[notified_of_own_activity]') - page.find('#user_notified_of_own_activity').set(true) + check 'user[notified_of_own_activity]' expect(page).to have_content('Notification settings saved') expect(page).to have_checked_field('user[notified_of_own_activity]') @@ -24,7 +24,7 @@ feature 'Profile > Notifications > User changes notified_of_own_activity setting expect(page).to have_checked_field('user[notified_of_own_activity]') - page.find('#user_notified_of_own_activity').set(false) + uncheck 'user[notified_of_own_activity]' expect(page).to have_content('Notification settings saved') expect(page).not_to have_checked_field('user[notified_of_own_activity]') From ac06070147f23909dfb5d3468a17a29e6b0cd447 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 2 Feb 2017 13:10:31 +0100 Subject: [PATCH 060/488] Use serializer to group environments into folders --- app/serializers/environment_serializer.rb | 39 +++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb index 91955542f25..8624392e07f 100644 --- a/app/serializers/environment_serializer.rb +++ b/app/serializers/environment_serializer.rb @@ -1,3 +1,42 @@ class EnvironmentSerializer < BaseSerializer + Struct.new('Item', :name, :size, :id, :latest) + entity EnvironmentEntity + + def with_folders + tap { @itemize = true } + end + + def itemized? + @itemize + end + + def represent(resource, opts = {}) + # resource = paginate(resource) if paginated? + + if itemized? + itemize(resource).map do |item| + { name: item.name, + size: item.size, + latest: super(item.latest, opts) } + end + else + super(resource, opts) + end + end + + private + + def itemize(resource) + items = resource.group(:item_name).order('item_name ASC') + .pluck('COALESCE(environment_type, name) AS item_name', + 'COUNT(*) AS environments_count', + 'MAX(id) AS last_environment_id') + + environments = resource.where(id: items.map(&:last)) + + items.zip(environments).map do |item| + Struct::Item.new(*item.flatten) + end + end end From 030adf12ce6dc8e10a9f0bbb34ff55aa818d7aed Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 2 Feb 2017 14:37:11 +0100 Subject: [PATCH 061/488] Encapsulate reused pagination component in a class --- app/serializers/environment_serializer.rb | 10 +++++- app/serializers/paginator.rb | 23 ++++++++++++++ app/serializers/pipeline_serializer.rb | 37 ++++++----------------- 3 files changed, 42 insertions(+), 28 deletions(-) create mode 100644 app/serializers/paginator.rb diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb index 8624392e07f..c556424a414 100644 --- a/app/serializers/environment_serializer.rb +++ b/app/serializers/environment_serializer.rb @@ -7,12 +7,20 @@ class EnvironmentSerializer < BaseSerializer tap { @itemize = true } end + def with_pagination(request, response) + tap { @paginator = Paginator.new(request, response) } + end + def itemized? @itemize end + def paginated? + defined?(@paginator) + end + def represent(resource, opts = {}) - # resource = paginate(resource) if paginated? + resource = @paginator.paginate(resource) if paginated? if itemized? itemize(resource).map do |item| diff --git a/app/serializers/paginator.rb b/app/serializers/paginator.rb new file mode 100644 index 00000000000..c5e38a9c8b5 --- /dev/null +++ b/app/serializers/paginator.rb @@ -0,0 +1,23 @@ +class Paginator + include API::Helpers::Pagination + + def initialize(request, response) + @request = request + @response = response + end + + private + + # Methods needed by `API::Helpers::Pagination` + # + + attr_reader :request + + def params + @request.query_parameters + end + + def header(header, value) + @response.headers[header] = value + end +end diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index b2de6c5832e..7c8dfad3b4b 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -1,41 +1,24 @@ class PipelineSerializer < BaseSerializer class InvalidResourceError < StandardError; end - include API::Helpers::Pagination - Struct.new('Pagination', :request, :response) entity PipelineEntity + def with_pagination(request, response) + tap { @paginator = Paginator.new(request, response) } + end + + def paginated? + defined?(@paginator) + end + def represent(resource, opts = {}) if paginated? raise InvalidResourceError unless resource.respond_to?(:page) - super(paginate(resource.includes(project: :namespace)), opts) + resource = resource.includes(project: :namespace) + super(@paginator.paginate(resource), opts) else super(resource, opts) end end - - def paginated? - defined?(@pagination) - end - - def with_pagination(request, response) - tap { @pagination = Struct::Pagination.new(request, response) } - end - - private - - # Methods needed by `API::Helpers::Pagination` - # - def params - @pagination.request.query_parameters - end - - def request - @pagination.request - end - - def header(header, value) - @pagination.response.headers[header] = value - end end From ca06c4b114e2fd65729d6473429758694abd5de1 Mon Sep 17 00:00:00 2001 From: JeJe Date: Thu, 2 Feb 2017 20:11:45 +0000 Subject: [PATCH 062/488] Update command-line-commands.md for issue #26428 --- doc/gitlab-basics/command-line-commands.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/gitlab-basics/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md index 3b075ff5fc0..741612be03c 100644 --- a/doc/gitlab-basics/command-line-commands.md +++ b/doc/gitlab-basics/command-line-commands.md @@ -25,6 +25,8 @@ git clone PASTE HTTPS OR SSH HERE A clone of the project will be created in your computer. +:warning: If you clone your project via an URL contains username/password combination, if it contains special characters they must be url encoded + ### Go into a project, directory or file to work in it ``` From 28bec57881fdd416170625a8a65ed5507e7a97a2 Mon Sep 17 00:00:00 2001 From: JeJe Date: Fri, 3 Feb 2017 00:48:31 +0000 Subject: [PATCH 063/488] use note instead of warning --- doc/gitlab-basics/command-line-commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/gitlab-basics/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md index 741612be03c..8a084eec96e 100644 --- a/doc/gitlab-basics/command-line-commands.md +++ b/doc/gitlab-basics/command-line-commands.md @@ -25,7 +25,7 @@ git clone PASTE HTTPS OR SSH HERE A clone of the project will be created in your computer. -:warning: If you clone your project via an URL contains username/password combination, if it contains special characters they must be url encoded +>**Note:** If you clone your project via an URL contains username/password combination, if it contains special characters they must be url encoded ### Go into a project, directory or file to work in it From f30e2a6ec7c011d0649aad9f118bf8c5a57ecdbc Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 3 Feb 2017 17:29:08 +0800 Subject: [PATCH 064/488] Use message_id_regexp variable for the regexp Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8620#note_22021001 --- lib/gitlab/incoming_email.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb index a386d9b36fb..0ea42148c58 100644 --- a/lib/gitlab/incoming_email.rb +++ b/lib/gitlab/incoming_email.rb @@ -35,7 +35,9 @@ module Gitlab end def key_from_fallback_message_id(mail_id) - mail_id[/\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/, 1] + message_id_regexp = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/ + + mail_id[message_id_regexp, 1] end def scan_fallback_references(references) From 849d09cfd692c0c54d45baf1ce71cd9c1ea2c6ab Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 3 Feb 2017 17:30:54 +0800 Subject: [PATCH 065/488] Use references variable Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8620#note_22020035 --- lib/gitlab/email/receiver.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index fa08b5c668f..b64db5d01ae 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -56,7 +56,9 @@ module Gitlab end def key_from_additional_headers(mail) - find_key_from_references(ensure_references_array(mail.references)) + references = ensure_references_array(mail.references) + + find_key_from_references(references) end def ensure_references_array(references) From 2f80cbb6759beb412491f9b1b4f0dbcbec6619c0 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 3 Feb 2017 17:37:06 +0800 Subject: [PATCH 066/488] Freeze regexp and add a comment Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8620#note_21590440 --- lib/gitlab/incoming_email.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb index 0ea42148c58..a492f904303 100644 --- a/lib/gitlab/incoming_email.rb +++ b/lib/gitlab/incoming_email.rb @@ -35,12 +35,14 @@ module Gitlab end def key_from_fallback_message_id(mail_id) - message_id_regexp = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/ + message_id_regexp = + /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/.freeze mail_id[message_id_regexp, 1] end def scan_fallback_references(references) + # It's looking for each <...> references.scan(/(?!<)[^<>]+(?=>)/.freeze) end From 037b4fe939696eebe6295a858470f2661d1e3878 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 27 Jan 2017 22:24:08 +0000 Subject: [PATCH 067/488] First iteration Create shared folder for vue common files Update paths Second iteration - refactor main component to be 100% reusable between the 3 tables --- .../commit/pipelines_bundle.js.es6 | 61 ++++ .../commit/pipelines_service.js.es6 | 77 +++++ .../javascripts/commit/pipelines_store.js.es6 | 30 ++ .../components/environment_item.js.es6 | 2 +- .../vue_pipelines_index/index.js.es6 | 2 +- .../components}/commit.js.es6 | 0 .../components/pipelines_table.js.es6 | 270 ++++++++++++++++++ .../vue_resource_interceptor.js.es6 | 10 + .../projects/commit/_pipelines_list.haml | 15 - app/views/projects/commit/pipelines.html.haml | 27 +- config/application.rb | 1 + .../vue_common_components/commit_spec.js.es6 | 2 +- 12 files changed, 478 insertions(+), 19 deletions(-) create mode 100644 app/assets/javascripts/commit/pipelines_bundle.js.es6 create mode 100644 app/assets/javascripts/commit/pipelines_service.js.es6 create mode 100644 app/assets/javascripts/commit/pipelines_store.js.es6 rename app/assets/javascripts/{vue_common_component => vue_shared/components}/commit.js.es6 (100%) create mode 100644 app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 create mode 100644 app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 diff --git a/app/assets/javascripts/commit/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines_bundle.js.es6 new file mode 100644 index 00000000000..d2547f0b4a8 --- /dev/null +++ b/app/assets/javascripts/commit/pipelines_bundle.js.es6 @@ -0,0 +1,61 @@ +/* eslint-disable no-new */ +/* global Vue, VueResource */ + +//= require vue +//= require vue-resource +//= require ./pipelines_store +//= require ./pipelines_service +//= require vue_shared/components/commit +//= require vue_shared/vue_resource_interceptor +//= require vue_shared/components/pipelines_table + +/** + * Commits View > Pipelines Tab > Pipelines Table. + * + * Renders Pipelines table in pipelines tab in the commits show view. + * + * Uses `pipelines-table-component` to render Pipelines table with an API call. + * Endpoint is provided in HTML and passed as scope. + * We need a store to make the request and store the received environemnts. + * + * Necessary SVG in the table are provided as props. This should be refactored + * as soon as we have Webpack and can load them directly into JS files. + */ +(() => { + window.gl = window.gl || {}; + gl.Commits = gl.Commits || {}; + + if (gl.Commits.PipelinesTableView) { + gl.Commits.PipelinesTableView.$destroy(true); + } + + gl.Commits.PipelinesTableView = new Vue({ + + el: document.querySelector('#commit-pipeline-table-view'), + + /** + * Accesses the DOM to provide the needed data. + * Returns the necessary props to render `pipelines-table-component` component. + * + * @return {Object} Props for `pipelines-table-component` + */ + data() { + const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; + + return { + scope: pipelinesTableData.pipelinesData, + store: new CommitsPipelineStore(), + service: new PipelinesService(), + svgs: pipelinesTableData, + }; + }, + + components: { + 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, + }, + + template: ` + + `, + }); +}); diff --git a/app/assets/javascripts/commit/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines_service.js.es6 new file mode 100644 index 00000000000..7d773e0d361 --- /dev/null +++ b/app/assets/javascripts/commit/pipelines_service.js.es6 @@ -0,0 +1,77 @@ +/* globals Vue */ +/* eslint-disable no-unused-vars, no-param-reassign */ + +/** + * Pipelines service. + * + * Used to fetch the data used to render the pipelines table. + * Used Vue.Resource + */ + +window.gl = window.gl || {}; +gl.pipelines = gl.pipelines || {}; + +class PipelinesService { + constructor(root) { + Vue.http.options.root = root; + + this.pipelines = Vue.resource(root); + + Vue.http.interceptors.push((request, next) => { + // needed in order to not break the tests. + if ($.rails) { + request.headers['X-CSRF-Token'] = $.rails.csrfToken(); + } + next(); + }); + } + + /** + * Given the root param provided when the class is initialized, will + * make a GET request. + * + * @return {Promise} + */ + all() { + return this.pipelines.get(); + } +} + +gl.pipelines.PipelinesService = PipelinesService; + +// const pageValues = (headers) => { +// const normalized = gl.utils.normalizeHeaders(headers); +// +// const paginationInfo = { +// perPage: +normalized['X-PER-PAGE'], +// page: +normalized['X-PAGE'], +// total: +normalized['X-TOTAL'], +// totalPages: +normalized['X-TOTAL-PAGES'], +// nextPage: +normalized['X-NEXT-PAGE'], +// previousPage: +normalized['X-PREV-PAGE'], +// }; +// +// return paginationInfo; +// }; + +// gl.PipelineStore = class { +// fetchDataLoop(Vue, pageNum, url, apiScope) { +// const goFetch = () => +// this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`) +// .then((response) => { +// const pageInfo = pageValues(response.headers); +// this.pageInfo = Object.assign({}, this.pageInfo, pageInfo); +// +// const res = JSON.parse(response.body); +// this.count = Object.assign({}, this.count, res.count); +// this.pipelines = Object.assign([], this.pipelines, res); +// +// this.pageRequest = false; +// }, () => { +// this.pageRequest = false; +// return new Flash('Something went wrong on our end.'); +// }); +// +// goFetch(); +// } +// }; diff --git a/app/assets/javascripts/commit/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines_store.js.es6 new file mode 100644 index 00000000000..bc748bece5d --- /dev/null +++ b/app/assets/javascripts/commit/pipelines_store.js.es6 @@ -0,0 +1,30 @@ +/* global gl, Flash */ +/* eslint-disable no-param-reassign, no-underscore-dangle */ +/*= require vue_realtime_listener/index.js */ + +/** + * Pipelines' Store for commits view. + * + * Used to store the Pipelines rendered in the commit view in the pipelines table. + * + * TODO: take care of timeago instances in here + */ + +(() => { + const CommitPipelineStore = { + state: {}, + + create() { + this.state.pipelines = []; + + return this; + }, + + storePipelines(pipelines = []) { + this.state.pipelines = pipelines; + return pipelines; + }, + }; + + return CommitPipelineStore; +})(); diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 0e6bc3fdb2c..c7cb0913213 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -3,7 +3,7 @@ /*= require timeago */ /*= require lib/utils/text_utility */ -/*= require vue_common_component/commit */ +/*= require vue_shared/components/commit */ /*= require ./environment_actions */ /*= require ./environment_external_url */ /*= require ./environment_stop */ diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6 index edd01f17a97..36f861a7d02 100644 --- a/app/assets/javascripts/vue_pipelines_index/index.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6 @@ -1,5 +1,5 @@ /* global Vue, VueResource, gl */ -/*= require vue_common_component/commit */ +/*= require vue_shared/components/commit */ /*= require vue_pagination/index */ /*= require vue-resource /*= require boards/vue_resource_interceptor */ diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_shared/components/commit.js.es6 similarity index 100% rename from app/assets/javascripts/vue_common_component/commit.js.es6 rename to app/assets/javascripts/vue_shared/components/commit.js.es6 diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 new file mode 100644 index 00000000000..0b20bf66a69 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 @@ -0,0 +1,270 @@ +/* eslint-disable no-param-reassign, no-new */ +/* global Vue */ +/* global PipelinesService */ +/* global Flash */ + +//= require vue_pipelines_index/status.js.es6 +//= require vue_pipelines_index/pipeline_url.js.es6 +//= require vue_pipelines_index/stage.js.es6 +//= require vue_pipelines_index/pipeline_actions.js.es6 +//= require vue_pipelines_index/time_ago.js.es6 +//= require vue_pipelines_index/pipelines.js.es6 + +(() => { + window.gl = window.gl || {}; + gl.pipelines = gl.pipelines || {}; + + gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', { + + props: { + + /** + * Stores the Pipelines to render. + * It's passed as a prop to allow different stores to use this Component. + * Different API calls can result in different responses, using a custom + * store allows us to use the same pipeline component. + */ + store: { + type: Object, + required: true, + default: () => ({}), + }, + + /** + * Will be used to fetch the needed data. + * This component is used in different and therefore different API calls + * to different endpoints will be made. To guarantee this is a reusable + * component, the endpoint must be provided. + */ + endpoint: { + type: String, + required: true, + }, + + /** + * Remove this. Find a better way to do this. don't want to provide this 3 times. + */ + svgs: { + type: Object, + required: true, + default: () => ({}), + }, + }, + + components: { + 'commit-component': gl.CommitComponent, + runningPipeline: gl.VueRunningPipeline, + pipelineActions: gl.VuePipelineActions, + 'vue-stage': gl.VueStage, + pipelineUrl: gl.VuePipelineUrl, + pipelineHead: gl.VuePipelineHead, + statusScope: gl.VueStatusScope, + }, + + data() { + return { + state: this.store.state, + isLoading: false, + }; + }, + + computed: { + /** + * If provided, returns the commit tag. + * + * @returns {Object|Undefined} + */ + commitAuthor() { + if (this.pipeline && + this.pipeline.commit && + this.pipeline.commit.author) { + return this.pipeline.commit.author; + } + + return undefined; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {String|Undefined} + */ + commitTag() { + if (this.model.last_deployment && + this.model.last_deployment.tag) { + return this.model.last_deployment.tag; + } + return undefined; + }, + + /** + * If provided, returns the commit ref. + * + * @returns {Object|Undefined} + */ + commitRef() { + if (this.pipeline.ref) { + return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { + if (prop === 'url') { + accumulator.path = this.pipeline.ref[prop]; + } else { + accumulator[prop] = this.pipeline.ref[prop]; + } + return accumulator; + }, {}); + } + + return undefined; + }, + + /** + * If provided, returns the commit url. + * + * @returns {String|Undefined} + */ + commitUrl() { + if (this.pipeline.commit && + this.pipeline.commit.commit_path) { + return this.pipeline.commit.commit_path; + } + return undefined; + }, + + /** + * If provided, returns the commit short sha. + * + * @returns {String|Undefined} + */ + commitShortSha() { + if (this.pipeline.commit && + this.pipeline.commit.short_id) { + return this.pipeline.commit.short_id; + } + return undefined; + }, + + /** + * If provided, returns the commit title. + * + * @returns {String|Undefined} + */ + commitTitle() { + if (this.pipeline.commit && + this.pipeline.commit.title) { + return this.pipeline.commit.title; + } + return undefined; + }, + + /** + * Figure this out! + */ + author(pipeline) { + if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; + if (pipeline.commit.author) return pipeline.commit.author; + return { + avatar_url: pipeline.commit.author_gravatar_url, + web_url: `mailto:${pipeline.commit.author_email}`, + username: pipeline.commit.author_name, + }; + }, + + /** + * Figure this out + */ + match(string) { + return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); + }, + }, + + /** + * When the component is created the service to fetch the data will be + * initialized with the correct endpoint. + * + * A request to fetch the pipelines will be made. + * In case of a successfull response we will store the data in the provided + * store, in case of a failed response we need to warn the user. + * + */ + created() { + gl.pipelines.pipelinesService = new PipelinesService(this.endpoint); + + this.isLoading = true; + + return gl.pipelines.pipelinesService.all() + .then(resp => resp.json()) + .then((json) => { + this.store.storePipelines(json); + this.isLoading = false; + }).catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the pipelines.', 'alert'); + }); + }, + // this need to be reusable between the 3 tables :/ + template: ` +
+
+ +
+ + +
+

+ You don't have any pipelines. +

+ Put get started with pipelines button here!!! +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
StatusPipelineCommitStages
+ + + + +
+
+
+ `, + }); +})(); diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 new file mode 100644 index 00000000000..54c2b4ad369 --- /dev/null +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 @@ -0,0 +1,10 @@ +/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars */ +/* global Vue */ + +Vue.http.interceptors.push((request, next) => { + Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; + + next(function (response) { + Vue.activeResources -= 1; + }); +}); diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 1164627fa11..e69de29bb2d 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -1,15 +0,0 @@ -%div - - if pipelines.blank? - %div - .nothing-here-block No pipelines to show - - else - .table-holder.pipelines - %table.table.ci-table.js-pipeline-table - %thead - %th.pipeline-status Status - %th.pipeline-info Pipeline - %th.pipeline-commit Commit - %th.pipeline-stages Stages - %th.pipeline-date - %th.pipeline-actions - = render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml index 89968cf4e0d..f4937a03f03 100644 --- a/app/views/projects/commit/pipelines.html.haml +++ b/app/views/projects/commit/pipelines.html.haml @@ -2,4 +2,29 @@ = render 'commit_box' = render 'ci_menu' -= render 'pipelines_list', pipelines: @pipelines + +- content_for :page_specific_javascripts do + = page_specific_javascript_tag("commit/pipelines_bundle.js") + +#commit-pipeline-table-view{ data: { pipelines_data: pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id)}} +.pipeline-svgs{ data: { "commit_icon_svg" => custom_icon("icon_commit"), + "icon_status_canceled" => custom_icon("icon_status_canceled"), + "icon_status_running" => custom_icon("icon_status_running"), + "icon_status_skipped" => custom_icon("icon_status_skipped"), + "icon_status_created" => custom_icon("icon_status_created"), + "icon_status_pending" => custom_icon("icon_status_pending"), + "icon_status_success" => custom_icon("icon_status_success"), + "icon_status_failed" => custom_icon("icon_status_failed"), + "icon_status_warning" => custom_icon("icon_status_warning"), + "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), + "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), + "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), + "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), + "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), + "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), + "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), + "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), + "icon_play" => custom_icon("icon_play"), + "icon_timer" => custom_icon("icon_timer"), + "icon_status_manual" => custom_icon("icon_status_manual"), +} } diff --git a/config/application.rb b/config/application.rb index f00e58a36ca..88c5e27d17d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -105,6 +105,7 @@ module Gitlab config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" config.assets.precompile << "boards/test_utils/simulate_drag.js" config.assets.precompile << "environments/environments_bundle.js" + config.assets.precompile << "commit/pipelines_bundle.js" config.assets.precompile << "blob_edit/blob_edit_bundle.js" config.assets.precompile << "snippet/snippet_bundle.js" config.assets.precompile << "terminal/terminal_bundle.js" diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6 index d6c6f786fb1..caf84ec63e2 100644 --- a/spec/javascripts/vue_common_components/commit_spec.js.es6 +++ b/spec/javascripts/vue_common_components/commit_spec.js.es6 @@ -1,4 +1,4 @@ -//= require vue_common_component/commit +//= require vue_shared/components/commit describe('Commit component', () => { let props; From 9972f59f94ab017d27d9278dd1c9dd89da489e64 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Sat, 28 Jan 2017 18:37:02 +0000 Subject: [PATCH 068/488] Use single source of truth for vue_resource_interceptor --- app/assets/javascripts/boards/boards_bundle.js.es6 | 2 +- .../javascripts/boards/vue_resource_interceptor.js.es6 | 10 ---------- .../javascripts/vue_pipelines_index/index.js.es6 | 2 +- .../vue_shared/components/pipelines_table.js.es6 | 5 ++++- 4 files changed, 6 insertions(+), 13 deletions(-) delete mode 100644 app/assets/javascripts/boards/vue_resource_interceptor.js.es6 diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index f9766471780..5b53cfe59cd 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -13,7 +13,7 @@ //= require ./components/board //= require ./components/board_sidebar //= require ./components/new_list_dropdown -//= require ./vue_resource_interceptor +//= require vue_shared/vue_resource_interceptor $(() => { const $boardApp = document.getElementById('board-app'); diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 deleted file mode 100644 index 54c2b4ad369..00000000000 --- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars */ -/* global Vue */ - -Vue.http.interceptors.push((request, next) => { - Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; - - next(function (response) { - Vue.activeResources -= 1; - }); -}); diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6 index 36f861a7d02..9ca7b1a746c 100644 --- a/app/assets/javascripts/vue_pipelines_index/index.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6 @@ -2,7 +2,7 @@ /*= require vue_shared/components/commit */ /*= require vue_pagination/index */ /*= require vue-resource -/*= require boards/vue_resource_interceptor */ +/*= require vue_shared/vue_resource_interceptor */ /*= require ./status.js.es6 */ /*= require ./store.js.es6 */ /*= require ./pipeline_url.js.es6 */ diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 index 0b20bf66a69..f602a0c44c2 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 @@ -19,10 +19,13 @@ props: { /** - * Stores the Pipelines to render. + * Object used to store the Pipelines to render. * It's passed as a prop to allow different stores to use this Component. * Different API calls can result in different responses, using a custom * store allows us to use the same pipeline component. + * + * Note: All provided stores need to have a `storePipelines` method. + * Find a better way to do this. */ store: { type: Object, From 7ad626e348faaea6f186759dada36079d531f6fd Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Sat, 28 Jan 2017 18:39:01 +0000 Subject: [PATCH 069/488] Use same folder structure in spec for vue shared resources --- .../javascripts/commit/{ => pipelines}/pipelines_bundle.js.es6 | 0 .../javascripts/commit/{ => pipelines}/pipelines_service.js.es6 | 0 .../javascripts/commit/{ => pipelines}/pipelines_store.js.es6 | 0 app/views/projects/commit/pipelines.html.haml | 2 +- config/application.rb | 2 +- .../components}/commit_spec.js.es6 | 0 6 files changed, 2 insertions(+), 2 deletions(-) rename app/assets/javascripts/commit/{ => pipelines}/pipelines_bundle.js.es6 (100%) rename app/assets/javascripts/commit/{ => pipelines}/pipelines_service.js.es6 (100%) rename app/assets/javascripts/commit/{ => pipelines}/pipelines_store.js.es6 (100%) rename spec/javascripts/{vue_common_components => vue_shared/components}/commit_spec.js.es6 (100%) diff --git a/app/assets/javascripts/commit/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 similarity index 100% rename from app/assets/javascripts/commit/pipelines_bundle.js.es6 rename to app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 diff --git a/app/assets/javascripts/commit/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 similarity index 100% rename from app/assets/javascripts/commit/pipelines_service.js.es6 rename to app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 diff --git a/app/assets/javascripts/commit/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 similarity index 100% rename from app/assets/javascripts/commit/pipelines_store.js.es6 rename to app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml index f4937a03f03..09bd4288b9c 100644 --- a/app/views/projects/commit/pipelines.html.haml +++ b/app/views/projects/commit/pipelines.html.haml @@ -4,7 +4,7 @@ = render 'ci_menu' - content_for :page_specific_javascripts do - = page_specific_javascript_tag("commit/pipelines_bundle.js") + = page_specific_javascript_tag("commit/pipelines/pipelines_bundle.js") #commit-pipeline-table-view{ data: { pipelines_data: pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id)}} .pipeline-svgs{ data: { "commit_icon_svg" => custom_icon("icon_commit"), diff --git a/config/application.rb b/config/application.rb index 88c5e27d17d..281a660ddee 100644 --- a/config/application.rb +++ b/config/application.rb @@ -105,7 +105,7 @@ module Gitlab config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" config.assets.precompile << "boards/test_utils/simulate_drag.js" config.assets.precompile << "environments/environments_bundle.js" - config.assets.precompile << "commit/pipelines_bundle.js" + config.assets.precompile << "commit/pipelines/pipelines_bundle.js" config.assets.precompile << "blob_edit/blob_edit_bundle.js" config.assets.precompile << "snippet/snippet_bundle.js" config.assets.precompile << "terminal/terminal_bundle.js" diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_shared/components/commit_spec.js.es6 similarity index 100% rename from spec/javascripts/vue_common_components/commit_spec.js.es6 rename to spec/javascripts/vue_shared/components/commit_spec.js.es6 From 7ef21460d1ad47c1e140b5cf2977ebc90f8c6dd1 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Sat, 28 Jan 2017 20:06:15 +0000 Subject: [PATCH 070/488] Transform vue_pipelines index into a non-dependent table component. --- .../commit/pipelines/pipelines_bundle.js.es6 | 85 +++++- .../commit/pipelines/pipelines_service.js.es6 | 45 +-- .../commit/pipelines/pipelines_store.js.es6 | 12 +- .../vue_pipelines_index/stage.js.es6 | 1 + .../components/pipelines_table.js.es6 | 277 +++--------------- .../components/pipelines_table_row.js.es6 | 192 ++++++++++++ app/views/projects/commit/pipelines.html.haml | 6 +- 7 files changed, 306 insertions(+), 312 deletions(-) create mode 100644 app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 index d2547f0b4a8..d42f2d15f19 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 @@ -1,11 +1,10 @@ /* eslint-disable no-new */ -/* global Vue, VueResource */ +/* global Vue, CommitsPipelineStore, PipelinesService, Flash */ +//= require vue +//= require_tree . //= require vue //= require vue-resource -//= require ./pipelines_store -//= require ./pipelines_service -//= require vue_shared/components/commit //= require vue_shared/vue_resource_interceptor //= require vue_shared/components/pipelines_table @@ -21,18 +20,23 @@ * Necessary SVG in the table are provided as props. This should be refactored * as soon as we have Webpack and can load them directly into JS files. */ -(() => { +$(() => { window.gl = window.gl || {}; - gl.Commits = gl.Commits || {}; + gl.commits = gl.commits || {}; + gl.commits.pipelines = gl.commits.pipelines || {}; - if (gl.Commits.PipelinesTableView) { - gl.Commits.PipelinesTableView.$destroy(true); + if (gl.commits.PipelinesTableView) { + gl.commits.PipelinesTableView.$destroy(true); } - gl.Commits.PipelinesTableView = new Vue({ + gl.commits.pipelines.PipelinesTableView = new Vue({ el: document.querySelector('#commit-pipeline-table-view'), + components: { + 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, + }, + /** * Accesses the DOM to provide the needed data. * Returns the necessary props to render `pipelines-table-component` component. @@ -41,21 +45,70 @@ */ data() { const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; + const svgsData = document.querySelector('.pipeline-svgs').dataset; + const store = gl.commits.pipelines.PipelinesStore.create(); + + // Transform svgs DOMStringMap to a plain Object. + const svgsObject = Object.keys(svgsData).reduce((acc, element) => { + acc[element] = svgsData[element]; + return acc; + }, {}); return { - scope: pipelinesTableData.pipelinesData, - store: new CommitsPipelineStore(), - service: new PipelinesService(), - svgs: pipelinesTableData, + endpoint: pipelinesTableData.pipelinesData, + svgs: svgsObject, + store, + state: store.state, + isLoading: false, }; }, - components: { - 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, + /** + * When the component is created the service to fetch the data will be + * initialized with the correct endpoint. + * + * A request to fetch the pipelines will be made. + * In case of a successfull response we will store the data in the provided + * store, in case of a failed response we need to warn the user. + * + */ + created() { + gl.pipelines.pipelinesService = new PipelinesService(this.endpoint); + + this.isLoading = true; + + return gl.pipelines.pipelinesService.all() + .then(response => response.json()) + .then((json) => { + this.store.store(json); + this.isLoading = false; + }).catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the pipelines.', 'alert'); + }); }, template: ` - +
+
+ +
+ +
+

+ You don't have any pipelines. +

+ Put get started with pipelines button here!!! +
+ +
+ + +
+
`, }); }); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 index 7d773e0d361..1e6aa73d9cf 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 @@ -5,12 +5,8 @@ * Pipelines service. * * Used to fetch the data used to render the pipelines table. - * Used Vue.Resource + * Uses Vue.Resource */ - -window.gl = window.gl || {}; -gl.pipelines = gl.pipelines || {}; - class PipelinesService { constructor(root) { Vue.http.options.root = root; @@ -36,42 +32,3 @@ class PipelinesService { return this.pipelines.get(); } } - -gl.pipelines.PipelinesService = PipelinesService; - -// const pageValues = (headers) => { -// const normalized = gl.utils.normalizeHeaders(headers); -// -// const paginationInfo = { -// perPage: +normalized['X-PER-PAGE'], -// page: +normalized['X-PAGE'], -// total: +normalized['X-TOTAL'], -// totalPages: +normalized['X-TOTAL-PAGES'], -// nextPage: +normalized['X-NEXT-PAGE'], -// previousPage: +normalized['X-PREV-PAGE'], -// }; -// -// return paginationInfo; -// }; - -// gl.PipelineStore = class { -// fetchDataLoop(Vue, pageNum, url, apiScope) { -// const goFetch = () => -// this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`) -// .then((response) => { -// const pageInfo = pageValues(response.headers); -// this.pageInfo = Object.assign({}, this.pageInfo, pageInfo); -// -// const res = JSON.parse(response.body); -// this.count = Object.assign({}, this.count, res.count); -// this.pipelines = Object.assign([], this.pipelines, res); -// -// this.pageRequest = false; -// }, () => { -// this.pageRequest = false; -// return new Flash('Something went wrong on our end.'); -// }); -// -// goFetch(); -// } -// }; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 index bc748bece5d..5c2e1b33cd1 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 @@ -6,12 +6,14 @@ * Pipelines' Store for commits view. * * Used to store the Pipelines rendered in the commit view in the pipelines table. - * - * TODO: take care of timeago instances in here */ (() => { - const CommitPipelineStore = { + window.gl = window.gl || {}; + gl.commits = gl.commits || {}; + gl.commits.pipelines = gl.commits.pipelines || {}; + + gl.commits.pipelines.PipelinesStore = { state: {}, create() { @@ -20,11 +22,9 @@ return this; }, - storePipelines(pipelines = []) { + store(pipelines = []) { this.state.pipelines = pipelines; return pipelines; }, }; - - return CommitPipelineStore; })(); diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 index 496df9aaced..572644c8e6e 100644 --- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 @@ -14,6 +14,7 @@ type: Object, required: true, }, + //FIXME: DOMStringMap is non standard, let's use a plain object. svgs: { type: DOMStringMap, required: true, diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 index f602a0c44c2..e606632306f 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 @@ -1,14 +1,14 @@ -/* eslint-disable no-param-reassign, no-new */ +/* eslint-disable no-param-reassign */ /* global Vue */ -/* global PipelinesService */ -/* global Flash */ -//= require vue_pipelines_index/status.js.es6 -//= require vue_pipelines_index/pipeline_url.js.es6 -//= require vue_pipelines_index/stage.js.es6 -//= require vue_pipelines_index/pipeline_actions.js.es6 -//= require vue_pipelines_index/time_ago.js.es6 -//= require vue_pipelines_index/pipelines.js.es6 +//=require ./pipelines_table_row + +/** + * Pipelines Table Component + * + * Given an array of pipelines, renders a table. + * + */ (() => { window.gl = window.gl || {}; @@ -17,31 +17,10 @@ gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', { props: { - - /** - * Object used to store the Pipelines to render. - * It's passed as a prop to allow different stores to use this Component. - * Different API calls can result in different responses, using a custom - * store allows us to use the same pipeline component. - * - * Note: All provided stores need to have a `storePipelines` method. - * Find a better way to do this. - */ - store: { - type: Object, - required: true, - default: () => ({}), - }, - - /** - * Will be used to fetch the needed data. - * This component is used in different and therefore different API calls - * to different endpoints will be made. To guarantee this is a reusable - * component, the endpoint must be provided. - */ - endpoint: { - type: String, + pipelines: { + type: Array, required: true, + default: [], }, /** @@ -55,219 +34,31 @@ }, components: { - 'commit-component': gl.CommitComponent, - runningPipeline: gl.VueRunningPipeline, - pipelineActions: gl.VuePipelineActions, - 'vue-stage': gl.VueStage, - pipelineUrl: gl.VuePipelineUrl, - pipelineHead: gl.VuePipelineHead, - statusScope: gl.VueStatusScope, + 'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent, }, - data() { - return { - state: this.store.state, - isLoading: false, - }; - }, - - computed: { - /** - * If provided, returns the commit tag. - * - * @returns {Object|Undefined} - */ - commitAuthor() { - if (this.pipeline && - this.pipeline.commit && - this.pipeline.commit.author) { - return this.pipeline.commit.author; - } - - return undefined; - }, - - /** - * If provided, returns the commit tag. - * - * @returns {String|Undefined} - */ - commitTag() { - if (this.model.last_deployment && - this.model.last_deployment.tag) { - return this.model.last_deployment.tag; - } - return undefined; - }, - - /** - * If provided, returns the commit ref. - * - * @returns {Object|Undefined} - */ - commitRef() { - if (this.pipeline.ref) { - return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { - if (prop === 'url') { - accumulator.path = this.pipeline.ref[prop]; - } else { - accumulator[prop] = this.pipeline.ref[prop]; - } - return accumulator; - }, {}); - } - - return undefined; - }, - - /** - * If provided, returns the commit url. - * - * @returns {String|Undefined} - */ - commitUrl() { - if (this.pipeline.commit && - this.pipeline.commit.commit_path) { - return this.pipeline.commit.commit_path; - } - return undefined; - }, - - /** - * If provided, returns the commit short sha. - * - * @returns {String|Undefined} - */ - commitShortSha() { - if (this.pipeline.commit && - this.pipeline.commit.short_id) { - return this.pipeline.commit.short_id; - } - return undefined; - }, - - /** - * If provided, returns the commit title. - * - * @returns {String|Undefined} - */ - commitTitle() { - if (this.pipeline.commit && - this.pipeline.commit.title) { - return this.pipeline.commit.title; - } - return undefined; - }, - - /** - * Figure this out! - */ - author(pipeline) { - if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; - if (pipeline.commit.author) return pipeline.commit.author; - return { - avatar_url: pipeline.commit.author_gravatar_url, - web_url: `mailto:${pipeline.commit.author_email}`, - username: pipeline.commit.author_name, - }; - }, - - /** - * Figure this out - */ - match(string) { - return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); - }, - }, - - /** - * When the component is created the service to fetch the data will be - * initialized with the correct endpoint. - * - * A request to fetch the pipelines will be made. - * In case of a successfull response we will store the data in the provided - * store, in case of a failed response we need to warn the user. - * - */ - created() { - gl.pipelines.pipelinesService = new PipelinesService(this.endpoint); - - this.isLoading = true; - - return gl.pipelines.pipelinesService.all() - .then(resp => resp.json()) - .then((json) => { - this.store.storePipelines(json); - this.isLoading = false; - }).catch(() => { - this.isLoading = false; - new Flash('An error occurred while fetching the pipelines.', 'alert'); - }); - }, - // this need to be reusable between the 3 tables :/ template: ` -
-
- -
- - -
-

- You don't have any pipelines. -

- Put get started with pipelines button here!!! -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
StatusPipelineCommitStages
- - - - -
-
-
+ + + + + + + + + + + + + + +
StatusPipelineCommitStages
`, }); })(); diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 new file mode 100644 index 00000000000..1e55cce1c41 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 @@ -0,0 +1,192 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +//= require vue_pipelines_index/status +//= require vue_pipelines_index/pipeline_url +//= require vue_pipelines_index/stage +//= require vue_shared/components/commit +//= require vue_pipelines_index/pipeline_actions +//= require vue_pipelines_index/time_ago +(() => { + window.gl = window.gl || {}; + gl.pipelines = gl.pipelines || {}; + + gl.pipelines.PipelinesTableRowComponent = Vue.component('pipelines-table-row-component', { + + props: { + pipeline: { + type: Object, + required: true, + default: () => ({}), + }, + + /** + * Remove this. Find a better way to do this. don't want to provide this 3 times. + */ + svgs: { + type: Object, + required: true, + default: () => ({}), + }, + }, + + components: { + 'commit-component': gl.CommitComponent, + runningPipeline: gl.VueRunningPipeline, + pipelineActions: gl.VuePipelineActions, + 'vue-stage': gl.VueStage, + pipelineUrl: gl.VuePipelineUrl, + pipelineHead: gl.VuePipelineHead, + statusScope: gl.VueStatusScope, + }, + + computed: { + /** + * If provided, returns the commit tag. + * Needed to render the commit component column. + * + * @returns {Object|Undefined} + */ + commitAuthor() { + if (this.pipeline && + this.pipeline.commit && + this.pipeline.commit.author) { + return this.pipeline.commit.author; + } + + return undefined; + }, + + /** + * If provided, returns the commit tag. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitTag() { + // if (this.model.last_deployment && + // this.model.last_deployment.tag) { + // return this.model.last_deployment.tag; + // } + return undefined; + }, + + /** + * If provided, returns the commit ref. + * Needed to render the commit component column. + * + * @returns {Object|Undefined} + */ + commitRef() { + if (this.pipeline.ref) { + return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { + if (prop === 'url') { + accumulator.path = this.pipeline.ref[prop]; + } else { + accumulator[prop] = this.pipeline.ref[prop]; + } + return accumulator; + }, {}); + } + + return undefined; + }, + + /** + * If provided, returns the commit url. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitUrl() { + if (this.pipeline.commit && + this.pipeline.commit.commit_path) { + return this.pipeline.commit.commit_path; + } + return undefined; + }, + + /** + * If provided, returns the commit short sha. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitShortSha() { + if (this.pipeline.commit && + this.pipeline.commit.short_id) { + return this.pipeline.commit.short_id; + } + return undefined; + }, + + /** + * If provided, returns the commit title. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitTitle() { + if (this.pipeline.commit && + this.pipeline.commit.title) { + return this.pipeline.commit.title; + } + return undefined; + }, + + /** + * Figure this out! + * Needed to render the commit component column. + */ + author(pipeline) { + if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; + if (pipeline.commit.author) return pipeline.commit.author; + return { + avatar_url: pipeline.commit.author_gravatar_url, + web_url: `mailto:${pipeline.commit.author_email}`, + username: pipeline.commit.author_name, + }; + }, + }, + + methods: { + match(string) { + return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); + }, + }, + + template: ` + + + + + + + + + + + + + + + + + + + + `, + }); +})(); diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml index 09bd4288b9c..f62fbe4d9cd 100644 --- a/app/views/projects/commit/pipelines.html.haml +++ b/app/views/projects/commit/pipelines.html.haml @@ -3,9 +3,6 @@ = render 'commit_box' = render 'ci_menu' -- content_for :page_specific_javascripts do - = page_specific_javascript_tag("commit/pipelines/pipelines_bundle.js") - #commit-pipeline-table-view{ data: { pipelines_data: pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id)}} .pipeline-svgs{ data: { "commit_icon_svg" => custom_icon("icon_commit"), "icon_status_canceled" => custom_icon("icon_status_canceled"), @@ -28,3 +25,6 @@ "icon_timer" => custom_icon("icon_timer"), "icon_status_manual" => custom_icon("icon_status_manual"), } } + +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('commit/pipelines/pipelines_bundle.js') From 2c2da2c07b775f1677456376d311560f1e43226f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Sat, 28 Jan 2017 21:26:04 +0000 Subject: [PATCH 071/488] Use new vue js pipelines table to render in merge request view Remove duplicate data-toggle attributes. Reuse the same pipeline table Remove unneeded required resources Remove unused file; Fix mr pipelines loading Updates documentation --- .../commit/pipelines/pipelines_bundle.js.es6 | 17 ++-- .../commit/pipelines/pipelines_store.js.es6 | 4 - app/assets/javascripts/dispatcher.js.es6 | 5 -- .../javascripts/merge_request_tabs.js.es6 | 24 ----- .../vue_pipelines_index/index.js.es6 | 43 ++++----- .../pipeline_actions.js.es6 | 12 +-- .../vue_pipelines_index/pipelines.js.es6 | 87 +++---------------- .../vue_pipelines_index/stage.js.es6 | 3 +- .../vue_pipelines_index/stages.js.es6 | 21 ----- .../components/pipelines_table.js.es6 | 2 +- .../components/pipelines_table_row.js.es6 | 55 ++++++++---- .../projects/merge_requests_controller.rb | 11 +-- .../projects/commit/_pipelines_list.haml | 25 ++++++ app/views/projects/commit/pipelines.html.haml | 27 +----- .../merge_requests/_new_submit.html.haml | 6 +- .../projects/merge_requests/_show.html.haml | 3 +- .../merge_requests/show/_pipelines.html.haml | 2 +- 17 files changed, 124 insertions(+), 223 deletions(-) delete mode 100644 app/assets/javascripts/vue_pipelines_index/stages.js.es6 diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 index d42f2d15f19..a06aad17824 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable no-new */ +/* eslint-disable no-new, no-param-reassign */ /* global Vue, CommitsPipelineStore, PipelinesService, Flash */ //= require vue @@ -10,8 +10,10 @@ /** * Commits View > Pipelines Tab > Pipelines Table. + * Merge Request View > Pipelines Tab > Pipelines Table. * * Renders Pipelines table in pipelines tab in the commits show view. + * Renders Pipelines table in pipelines tab in the merge request show view. * * Uses `pipelines-table-component` to render Pipelines table with an API call. * Endpoint is provided in HTML and passed as scope. @@ -20,6 +22,7 @@ * Necessary SVG in the table are provided as props. This should be refactored * as soon as we have Webpack and can load them directly into JS files. */ + $(() => { window.gl = window.gl || {}; gl.commits = gl.commits || {}; @@ -55,11 +58,12 @@ $(() => { }, {}); return { - endpoint: pipelinesTableData.pipelinesData, + endpoint: pipelinesTableData.endpoint, svgs: svgsObject, store, state: store.state, isLoading: false, + error: false, }; }, @@ -82,7 +86,9 @@ $(() => { .then((json) => { this.store.store(json); this.isLoading = false; + this.error = false; }).catch(() => { + this.error = true; this.isLoading = false; new Flash('An error occurred while fetching the pipelines.', 'alert'); }); @@ -95,14 +101,15 @@ $(() => {
+ v-if="!isLoading && !error && state.pipelines.length === 0">

You don't have any pipelines.

- Put get started with pipelines button here!!!
-
+
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 index 5c2e1b33cd1..b7d8e97fed3 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 @@ -1,7 +1,3 @@ -/* global gl, Flash */ -/* eslint-disable no-param-reassign, no-underscore-dangle */ -/*= require vue_realtime_listener/index.js */ - /** * Pipelines' Store for commits view. * diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index edec21e3b63..70f467d608f 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -159,11 +159,6 @@ new ZenMode(); shortcut_handler = new ShortcutsNavigation(); break; - case 'projects:commit:pipelines': - new gl.MiniPipelineGraph({ - container: '.js-pipeline-table', - }); - break; case 'projects:commits:show': case 'projects:activity': shortcut_handler = new ShortcutsNavigation(); diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 4c8c28af755..dabba9e1fa9 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -61,7 +61,6 @@ constructor({ action, setUrl, stubLocation } = {}) { this.diffsLoaded = false; - this.pipelinesLoaded = false; this.commitsLoaded = false; this.fixedLayoutPref = null; @@ -116,10 +115,6 @@ $.scrollTo('.merge-request-details .merge-request-tabs', { offset: -navBarHeight, }); - } else if (action === 'pipelines') { - this.loadPipelines($target.attr('href')); - this.expandView(); - this.resetViewContainer(); } else { this.expandView(); this.resetViewContainer(); @@ -243,25 +238,6 @@ }); } - loadPipelines(source) { - if (this.pipelinesLoaded) { - return; - } - this.ajaxGet({ - url: `${source}.json`, - success: (data) => { - $('#pipelines').html(data.html); - gl.utils.localTimeAgo($('.js-timeago', '#pipelines')); - this.pipelinesLoaded = true; - this.scrollToElement('#pipelines'); - - new gl.MiniPipelineGraph({ - container: '.js-pipeline-table', - }); - }, - }); - } - // Show or hide the loading spinner // // status - Boolean, true to show, false to hide diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6 index 9ca7b1a746c..e5359ba5398 100644 --- a/app/assets/javascripts/vue_pipelines_index/index.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6 @@ -1,31 +1,32 @@ +/* eslint-disable no-param-reassign */ /* global Vue, VueResource, gl */ -/*= require vue_shared/components/commit */ -/*= require vue_pagination/index */ + +//= require vue /*= require vue-resource /*= require vue_shared/vue_resource_interceptor */ -/*= require ./status.js.es6 */ -/*= require ./store.js.es6 */ -/*= require ./pipeline_url.js.es6 */ -/*= require ./stage.js.es6 */ -/*= require ./stages.js.es6 */ -/*= require ./pipeline_actions.js.es6 */ -/*= require ./time_ago.js.es6 */ /*= require ./pipelines.js.es6 */ -(() => { - const project = document.querySelector('.pipelines'); - const entry = document.querySelector('.vue-pipelines-index'); - const svgs = document.querySelector('.pipeline-svgs'); - +$(() => { Vue.use(VueResource); - if (!entry) return null; return new Vue({ - el: entry, - data: { - scope: project.dataset.url, - store: new gl.PipelineStore(), - svgs: svgs.dataset, + el: document.querySelector('.vue-pipelines-index'), + + data() { + const project = document.querySelector('.pipelines'); + const svgs = document.querySelector('.pipeline-svgs').dataset; + + // Transform svgs DOMStringMap to a plain Object. + const svgsObject = Object.keys(svgs).reduce((acc, element) => { + acc[element] = svgs[element]; + return acc; + }, {}); + + return { + scope: project.dataset.url, + store: new gl.PipelineStore(), + svgs: svgsObject, + }; }, components: { 'vue-pipelines': gl.VuePipelines, @@ -39,4 +40,4 @@ `, }); -})(); +}); diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 index a7176e27ea1..9b4897b1a9e 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -25,7 +25,6 @@
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 index b2ed05503c9..34d93ce1b7f 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -1,19 +1,18 @@ /* global Vue, Turbolinks, gl */ /* eslint-disable no-param-reassign */ +//= require vue_pagination/index +//= require ./store.js.es6 +//= require vue_shared/components/pipelines_table + ((gl) => { gl.VuePipelines = Vue.extend({ + components: { - runningPipeline: gl.VueRunningPipeline, - pipelineActions: gl.VuePipelineActions, - stages: gl.VueStages, - commit: gl.CommitComponent, - pipelineUrl: gl.VuePipelineUrl, - pipelineHead: gl.VuePipelineHead, glPagination: gl.VueGlPagination, - statusScope: gl.VueStatusScope, - timeAgo: gl.VueTimeAgo, + 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, }, + data() { return { pipelines: [], @@ -38,31 +37,6 @@ change(pagenum, apiScope) { Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); }, - author(pipeline) { - if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; - if (pipeline.commit.author) return pipeline.commit.author; - return { - avatar_url: pipeline.commit.author_gravatar_url, - web_url: `mailto:${pipeline.commit.author_email}`, - username: pipeline.commit.author_name, - }; - }, - ref(pipeline) { - const { ref } = pipeline; - return { name: ref.name, tag: ref.tag, ref_url: ref.path }; - }, - commitTitle(pipeline) { - return pipeline.commit ? pipeline.commit.title : ''; - }, - commitSha(pipeline) { - return pipeline.commit ? pipeline.commit.short_id : ''; - }, - commitUrl(pipeline) { - return pipeline.commit ? pipeline.commit.commit_path : ''; - }, - match(string) { - return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); - }, }, template: `
@@ -70,49 +44,10 @@
- - - - - - - - - - - - - - - - - - - - - - - -
StatusPipelineCommitStages
- - -
+ +
diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 index 572644c8e6e..8cc417a9966 100644 --- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 @@ -14,9 +14,8 @@ type: Object, required: true, }, - //FIXME: DOMStringMap is non standard, let's use a plain object. svgs: { - type: DOMStringMap, + type: Object, required: true, }, match: { diff --git a/app/assets/javascripts/vue_pipelines_index/stages.js.es6 b/app/assets/javascripts/vue_pipelines_index/stages.js.es6 deleted file mode 100644 index cb176b3f0c6..00000000000 --- a/app/assets/javascripts/vue_pipelines_index/stages.js.es6 +++ /dev/null @@ -1,21 +0,0 @@ -/* global Vue, gl */ -/* eslint-disable no-param-reassign */ - -((gl) => { - gl.VueStages = Vue.extend({ - components: { - 'vue-stage': gl.VueStage, - }, - props: ['pipeline', 'svgs', 'match'], - template: ` - - - - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 index e606632306f..4b6bba461d7 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 @@ -1,7 +1,7 @@ /* eslint-disable no-param-reassign */ /* global Vue */ -//=require ./pipelines_table_row +//= require ./pipelines_table_row /** * Pipelines Table Component diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 index 1e55cce1c41..c0ff0c90e4e 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 @@ -38,6 +38,7 @@ pipelineUrl: gl.VuePipelineUrl, pipelineHead: gl.VuePipelineHead, statusScope: gl.VueStatusScope, + 'time-ago': gl.VueTimeAgo, }, computed: { @@ -45,18 +46,50 @@ * If provided, returns the commit tag. * Needed to render the commit component column. * + * TODO: Document this logic, need to ask @grzesiek and @selfup + * * @returns {Object|Undefined} */ commitAuthor() { + if (!this.pipeline.commit) { + return { avatar_url: '', web_url: '', username: '' }; + } + if (this.pipeline && this.pipeline.commit && this.pipeline.commit.author) { return this.pipeline.commit.author; } + if (this.pipeline && + this.pipeline.commit && + this.pipeline.commit.author_gravatar_url && + this.pipeline.commit.author_name && + this.pipeline.commit.author_email) { + return { + avatar_url: this.pipeline.commit.author_gravatar_url, + web_url: `mailto:${this.pipeline.commit.author_email}`, + username: this.pipeline.commit.author_name, + }; + } + return undefined; }, + /** + * Figure this out! + * Needed to render the commit component column. + */ + author(pipeline) { + if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; + if (pipeline.commit.author) return pipeline.commit.author; + return { + avatar_url: pipeline.commit.author_gravatar_url, + web_url: `mailto:${pipeline.commit.author_email}`, + username: pipeline.commit.author_name, + }; + }, + /** * If provided, returns the commit tag. * Needed to render the commit component column. @@ -64,10 +97,10 @@ * @returns {String|Undefined} */ commitTag() { - // if (this.model.last_deployment && - // this.model.last_deployment.tag) { - // return this.model.last_deployment.tag; - // } + if (this.pipeline.ref && + this.pipeline.ref.tag) { + return this.pipeline.ref.tag; + } return undefined; }, @@ -133,20 +166,6 @@ } return undefined; }, - - /** - * Figure this out! - * Needed to render the commit component column. - */ - author(pipeline) { - if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; - if (pipeline.commit.author) return pipeline.commit.author; - return { - avatar_url: pipeline.commit.author_gravatar_url, - web_url: `mailto:${pipeline.commit.author_email}`, - username: pipeline.commit.author_name, - }; - }, }, methods: { diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 6eb542e4bd8..deb084c2e91 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -216,13 +216,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController end format.json do - render json: { - html: view_to_html_string('projects/merge_requests/show/_pipelines'), - pipelines: PipelineSerializer - .new(project: @project, user: @current_user) - .with_pagination(request, response) - .represent(@pipelines) - } + render json: PipelineSerializer + .new(project: @project, user: @current_user) + .with_pagination(request, response) + .represent(@pipelines) end end end diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index e69de29bb2d..bfe5eb18ad1 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -0,0 +1,25 @@ +#commit-pipeline-table-view{ data: { endpoint: endpoint } } +.pipeline-svgs{ data: { "commit_icon_svg" => custom_icon("icon_commit"), + "icon_status_canceled" => custom_icon("icon_status_canceled"), + "icon_status_running" => custom_icon("icon_status_running"), + "icon_status_skipped" => custom_icon("icon_status_skipped"), + "icon_status_created" => custom_icon("icon_status_created"), + "icon_status_pending" => custom_icon("icon_status_pending"), + "icon_status_success" => custom_icon("icon_status_success"), + "icon_status_failed" => custom_icon("icon_status_failed"), + "icon_status_warning" => custom_icon("icon_status_warning"), + "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), + "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), + "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), + "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), + "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), + "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), + "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), + "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), + "icon_play" => custom_icon("icon_play"), + "icon_timer" => custom_icon("icon_timer"), + "icon_status_manual" => custom_icon("icon_status_manual"), +} } + +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('commit/pipelines/pipelines_bundle.js') diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml index f62fbe4d9cd..ac93eac41ac 100644 --- a/app/views/projects/commit/pipelines.html.haml +++ b/app/views/projects/commit/pipelines.html.haml @@ -2,29 +2,4 @@ = render 'commit_box' = render 'ci_menu' - -#commit-pipeline-table-view{ data: { pipelines_data: pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id)}} -.pipeline-svgs{ data: { "commit_icon_svg" => custom_icon("icon_commit"), - "icon_status_canceled" => custom_icon("icon_status_canceled"), - "icon_status_running" => custom_icon("icon_status_running"), - "icon_status_skipped" => custom_icon("icon_status_skipped"), - "icon_status_created" => custom_icon("icon_status_created"), - "icon_status_pending" => custom_icon("icon_status_pending"), - "icon_status_success" => custom_icon("icon_status_success"), - "icon_status_failed" => custom_icon("icon_status_failed"), - "icon_status_warning" => custom_icon("icon_status_warning"), - "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), - "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), - "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), - "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), - "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), - "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), - "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), - "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), - "icon_play" => custom_icon("icon_play"), - "icon_timer" => custom_icon("icon_timer"), - "icon_status_manual" => custom_icon("icon_status_manual"), -} } - -- content_for :page_specific_javascripts do - = page_specific_javascript_tag('commit/pipelines/pipelines_bundle.js') += render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index d3c013b3f21..c1f48837e0e 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -44,9 +44,9 @@ = render "projects/merge_requests/show/commits" #diffs.diffs.tab-pane -# This tab is always loaded via AJAX - - if @pipelines.any? - #pipelines.pipelines.tab-pane - = render "projects/merge_requests/show/pipelines" + #pipelines.pipelines.tab-pane + //TODO: This needs to make a new request every time is opened! + = render "projects/merge_requests/show/pipelines", endpoint: link_to url_for(params) .mr-loading-status = spinner diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 9585a9a3ad4..8dfe967a937 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -94,7 +94,8 @@ #commits.commits.tab-pane -# This tab is always loaded via AJAX #pipelines.pipelines.tab-pane - -# This tab is always loaded via AJAX + //TODO: This needs to make a new request every time is opened! + = render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) #diffs.diffs.tab-pane -# This tab is always loaded via AJAX diff --git a/app/views/projects/merge_requests/show/_pipelines.html.haml b/app/views/projects/merge_requests/show/_pipelines.html.haml index afe3f3430c6..cbe534abedb 100644 --- a/app/views/projects/merge_requests/show/_pipelines.html.haml +++ b/app/views/projects/merge_requests/show/_pipelines.html.haml @@ -1 +1 @@ -= render "projects/commit/pipelines_list", pipelines: @pipelines, link_to_commit: true += render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) From 184f60a06f828ccbc9264d40e6daa48d60dca629 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Sun, 29 Jan 2017 15:30:04 +0000 Subject: [PATCH 072/488] Moves pagination to shared folder Document and remove unused code Declare components in a consistent way; Use " instead of ' to improve consistency; Update documentation; Fix commit author verification to match the use cases; Adds tests for the added components Fix paths in pagination spec Adds tests to pipelines table used in merge requests and commits Use same resource interceptor Fix eslint error --- .../commit/pipelines/pipelines_bundle.js.es6 | 100 +--------------- .../commit/pipelines/pipelines_service.js.es6 | 5 + .../commit/pipelines/pipelines_table.js.es6 | 104 +++++++++++++++++ .../environments/environments_bundle.js.es6 | 2 +- .../vue_resource_interceptor.js.es6 | 12 -- .../vue_pipelines_index/pipelines.js.es6 | 4 +- .../vue_pipelines_index/time_ago.js.es6 | 2 + .../components/pipelines_table.js.es6 | 24 ++-- .../components/pipelines_table_row.js.es6 | 102 ++++++++++------- .../components/table_pagination.js.es6} | 0 .../vue_resource_interceptor.js.es6 | 11 +- app/controllers/projects/commit_controller.rb | 1 - .../projects/merge_requests_controller.rb | 1 - .../merge_requests/_new_submit.html.haml | 6 +- .../projects/merge_requests/_show.html.haml | 1 - .../commit/pipelines/mock_data.js.es6 | 90 +++++++++++++++ .../commit/pipelines/pipelines_spec.js.es6 | 107 ++++++++++++++++++ .../pipelines/pipelines_store_spec.js.es6 | 31 +++++ .../fixtures/pipelines_table.html.haml | 2 + .../pipelines_table_row_spec.js.es6 | 90 +++++++++++++++ .../components/pipelines_table_spec.js.es6 | 67 +++++++++++ .../components/table_pagination_spec.js.es6} | 3 +- 22 files changed, 591 insertions(+), 174 deletions(-) create mode 100644 app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 delete mode 100644 app/assets/javascripts/environments/vue_resource_interceptor.js.es6 rename app/assets/javascripts/{vue_pagination/index.js.es6 => vue_shared/components/table_pagination.js.es6} (100%) create mode 100644 spec/javascripts/commit/pipelines/mock_data.js.es6 create mode 100644 spec/javascripts/commit/pipelines/pipelines_spec.js.es6 create mode 100644 spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 create mode 100644 spec/javascripts/fixtures/pipelines_table.html.haml create mode 100644 spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 create mode 100644 spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 rename spec/javascripts/{vue_pagination/pagination_spec.js.es6 => vue_shared/components/table_pagination_spec.js.es6} (98%) diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 index a06aad17824..b21d13842a4 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 @@ -3,10 +3,6 @@ //= require vue //= require_tree . -//= require vue -//= require vue-resource -//= require vue_shared/vue_resource_interceptor -//= require vue_shared/components/pipelines_table /** * Commits View > Pipelines Tab > Pipelines Table. @@ -14,13 +10,6 @@ * * Renders Pipelines table in pipelines tab in the commits show view. * Renders Pipelines table in pipelines tab in the merge request show view. - * - * Uses `pipelines-table-component` to render Pipelines table with an API call. - * Endpoint is provided in HTML and passed as scope. - * We need a store to make the request and store the received environemnts. - * - * Necessary SVG in the table are provided as props. This should be refactored - * as soon as we have Webpack and can load them directly into JS files. */ $(() => { @@ -28,94 +17,11 @@ $(() => { gl.commits = gl.commits || {}; gl.commits.pipelines = gl.commits.pipelines || {}; - if (gl.commits.PipelinesTableView) { - gl.commits.PipelinesTableView.$destroy(true); + if (gl.commits.PipelinesTableBundle) { + gl.commits.PipelinesTableBundle.$destroy(true); } - gl.commits.pipelines.PipelinesTableView = new Vue({ - + gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView({ el: document.querySelector('#commit-pipeline-table-view'), - - components: { - 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, - }, - - /** - * Accesses the DOM to provide the needed data. - * Returns the necessary props to render `pipelines-table-component` component. - * - * @return {Object} Props for `pipelines-table-component` - */ - data() { - const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; - const svgsData = document.querySelector('.pipeline-svgs').dataset; - const store = gl.commits.pipelines.PipelinesStore.create(); - - // Transform svgs DOMStringMap to a plain Object. - const svgsObject = Object.keys(svgsData).reduce((acc, element) => { - acc[element] = svgsData[element]; - return acc; - }, {}); - - return { - endpoint: pipelinesTableData.endpoint, - svgs: svgsObject, - store, - state: store.state, - isLoading: false, - error: false, - }; - }, - - /** - * When the component is created the service to fetch the data will be - * initialized with the correct endpoint. - * - * A request to fetch the pipelines will be made. - * In case of a successfull response we will store the data in the provided - * store, in case of a failed response we need to warn the user. - * - */ - created() { - gl.pipelines.pipelinesService = new PipelinesService(this.endpoint); - - this.isLoading = true; - - return gl.pipelines.pipelinesService.all() - .then(response => response.json()) - .then((json) => { - this.store.store(json); - this.isLoading = false; - this.error = false; - }).catch(() => { - this.error = true; - this.isLoading = false; - new Flash('An error occurred while fetching the pipelines.', 'alert'); - }); - }, - - template: ` -
-
- -
- -
-

- You don't have any pipelines. -

-
- -
- - -
-
- `, }); }); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 index 1e6aa73d9cf..f4ed986b0c5 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 @@ -32,3 +32,8 @@ class PipelinesService { return this.pipelines.get(); } } + +window.gl = window.gl || {}; +gl.commits = gl.commits || {}; +gl.commits.pipelines = gl.commits.pipelines || {}; +gl.commits.pipelines.PipelinesService = PipelinesService; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 new file mode 100644 index 00000000000..df7a6455eed --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 @@ -0,0 +1,104 @@ +/* eslint-disable no-new, no-param-reassign */ +/* global Vue, CommitsPipelineStore, PipelinesService, Flash */ + +//= require vue +//= require vue-resource +//= require vue_shared/vue_resource_interceptor +//= require vue_shared/components/pipelines_table + +/** + * + * Uses `pipelines-table-component` to render Pipelines table with an API call. + * Endpoint is provided in HTML and passed as `endpoint`. + * We need a store to store the received environemnts. + * We need a service to communicate with the server. + * + * Necessary SVG in the table are provided as props. This should be refactored + * as soon as we have Webpack and can load them directly into JS files. + */ + +(() => { + window.gl = window.gl || {}; + gl.commits = gl.commits || {}; + gl.commits.pipelines = gl.commits.pipelines || {}; + + gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', { + + components: { + 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, + }, + + /** + * Accesses the DOM to provide the needed data. + * Returns the necessary props to render `pipelines-table-component` component. + * + * @return {Object} + */ + data() { + const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; + const svgsData = document.querySelector('.pipeline-svgs').dataset; + const store = gl.commits.pipelines.PipelinesStore.create(); + + // Transform svgs DOMStringMap to a plain Object. + const svgsObject = Object.keys(svgsData).reduce((acc, element) => { + acc[element] = svgsData[element]; + return acc; + }, {}); + + return { + endpoint: pipelinesTableData.endpoint, + svgs: svgsObject, + store, + state: store.state, + isLoading: false, + }; + }, + + /** + * When the component is created the service to fetch the data will be + * initialized with the correct endpoint. + * + * A request to fetch the pipelines will be made. + * In case of a successfull response we will store the data in the provided + * store, in case of a failed response we need to warn the user. + * + */ + created() { + gl.pipelines.pipelinesService = new PipelinesService(this.endpoint); + + this.isLoading = true; + return gl.pipelines.pipelinesService.all() + .then(response => response.json()) + .then((json) => { + this.store.store(json); + this.isLoading = false; + }).catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the pipelines.', 'alert'); + }); + }, + + template: ` +
+
+ +
+ +
+

+ No pipelines to show +

+
+ +
+ + +
+
+ `, + }); +})(); diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 3b003f6f661..cd205617a97 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -1,7 +1,7 @@ //= require vue //= require_tree ./stores/ //= require ./components/environment -//= require ./vue_resource_interceptor +//= require vue_shared/vue_resource_interceptor $(() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 deleted file mode 100644 index 406bdbc1c7d..00000000000 --- a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 +++ /dev/null @@ -1,12 +0,0 @@ -/* global Vue */ -Vue.http.interceptors.push((request, next) => { - Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; - - next((response) => { - if (typeof response.data === 'string') { - response.data = JSON.parse(response.data); // eslint-disable-line - } - - Vue.activeResources--; // eslint-disable-line - }); -}); diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 index 34d93ce1b7f..c1daf816060 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -1,7 +1,7 @@ /* global Vue, Turbolinks, gl */ /* eslint-disable no-param-reassign */ -//= require vue_pagination/index +//= require vue_shared/components/table_pagination //= require ./store.js.es6 //= require vue_shared/components/pipelines_table @@ -9,7 +9,7 @@ gl.VuePipelines = Vue.extend({ components: { - glPagination: gl.VueGlPagination, + 'gl-pagination': gl.VueGlPagination, 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, }, diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 index 655110feba1..61417b28630 100644 --- a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 @@ -1,6 +1,8 @@ /* global Vue, gl */ /* eslint-disable no-param-reassign */ +//= require lib/utils/datetime_utility + ((gl) => { gl.VueTimeAgo = Vue.extend({ data() { diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 index 4b6bba461d7..9bc1ea65e53 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 @@ -4,10 +4,9 @@ //= require ./pipelines_table_row /** - * Pipelines Table Component - * - * Given an array of pipelines, renders a table. + * Pipelines Table Component. * + * Given an array of objects, renders a table. */ (() => { @@ -20,11 +19,11 @@ pipelines: { type: Array, required: true, - default: [], + default: () => ([]), }, /** - * Remove this. Find a better way to do this. don't want to provide this 3 times. + * TODO: Remove this when we have webpack. */ svgs: { type: Object, @@ -41,19 +40,18 @@ - - - - - - + + + + + + diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 index c0ff0c90e4e..375516e3804 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 @@ -7,6 +7,12 @@ //= require vue_shared/components/commit //= require vue_pipelines_index/pipeline_actions //= require vue_pipelines_index/time_ago + +/** + * Pipeline table row. + * + * Given the received object renders a table row in the pipelines' table. + */ (() => { window.gl = window.gl || {}; gl.pipelines = gl.pipelines || {}; @@ -21,7 +27,7 @@ }, /** - * Remove this. Find a better way to do this. don't want to provide this 3 times. + * TODO: Remove this when we have webpack; */ svgs: { type: Object, @@ -32,12 +38,10 @@ components: { 'commit-component': gl.CommitComponent, - runningPipeline: gl.VueRunningPipeline, - pipelineActions: gl.VuePipelineActions, - 'vue-stage': gl.VueStage, - pipelineUrl: gl.VuePipelineUrl, - pipelineHead: gl.VuePipelineHead, - statusScope: gl.VueStatusScope, + 'pipeline-actions': gl.VuePipelineActions, + 'dropdown-stage': gl.VueStage, + 'pipeline-url': gl.VuePipelineUrl, + 'status-scope': gl.VueStatusScope, 'time-ago': gl.VueTimeAgo, }, @@ -46,48 +50,48 @@ * If provided, returns the commit tag. * Needed to render the commit component column. * - * TODO: Document this logic, need to ask @grzesiek and @selfup + * This field needs a lot of verification, because of different possible cases: + * + * 1. person who is an author of a commit might be a GitLab user + * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar + * 3. If GitLab user does not have avatar he/she might have a Gravatar + * 4. If committer is not a GitLab User he/she can have a Gravatar + * 5. We do not have consistent API object in this case + * 6. We should improve API and the code * * @returns {Object|Undefined} */ commitAuthor() { - if (!this.pipeline.commit) { - return { avatar_url: '', web_url: '', username: '' }; - } + let commitAuthorInformation; + // 1. person who is an author of a commit might be a GitLab user if (this.pipeline && this.pipeline.commit && this.pipeline.commit.author) { - return this.pipeline.commit.author; + // 2. if person who is an author of a commit is a GitLab user + // he/she can have a GitLab avatar + if (this.pipeline.commit.author.avatar_url) { + commitAuthorInformation = this.pipeline.commit.author; + + // 3. If GitLab user does not have avatar he/she might have a Gravatar + } else if (this.pipeline.commit.author_gravatar_url) { + commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, { + avatar_url: this.pipeline.commit.author_gravatar_url, + }); + } } + // 4. If committer is not a GitLab User he/she can have a Gravatar if (this.pipeline && - this.pipeline.commit && - this.pipeline.commit.author_gravatar_url && - this.pipeline.commit.author_name && - this.pipeline.commit.author_email) { - return { + this.pipeline.commit) { + commitAuthorInformation = { avatar_url: this.pipeline.commit.author_gravatar_url, web_url: `mailto:${this.pipeline.commit.author_email}`, username: this.pipeline.commit.author_name, }; } - return undefined; - }, - - /** - * Figure this out! - * Needed to render the commit component column. - */ - author(pipeline) { - if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; - if (pipeline.commit.author) return pipeline.commit.author; - return { - avatar_url: pipeline.commit.author_gravatar_url, - web_url: `mailto:${pipeline.commit.author_email}`, - username: pipeline.commit.author_name, - }; + return commitAuthorInformation; }, /** @@ -108,6 +112,9 @@ * If provided, returns the commit ref. * Needed to render the commit component column. * + * Matched `url` prop sent in the API to `path` prop needed + * in the commit component. + * * @returns {Object|Undefined} */ commitRef() { @@ -169,6 +176,17 @@ }, methods: { + /** + * FIXME: This should not be in this component but in the components that + * need this function. + * + * Used to render SVGs in the following components: + * - status-scope + * - dropdown-stage + * + * @param {String} string + * @return {String} + */ match(string) { return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); }, @@ -177,12 +195,12 @@ template: ` - + - + - + `, }); diff --git a/app/assets/javascripts/vue_pagination/index.js.es6 b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 similarity index 100% rename from app/assets/javascripts/vue_pagination/index.js.es6 rename to app/assets/javascripts/vue_shared/components/table_pagination.js.es6 diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 index 54c2b4ad369..d627fa2b88a 100644 --- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 @@ -1,10 +1,15 @@ -/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars */ +/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars, +no-param-reassign, no-plusplus */ /* global Vue */ Vue.http.interceptors.push((request, next) => { Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; - next(function (response) { - Vue.activeResources -= 1; + next((response) => { + if (typeof response.data === 'string') { + response.data = JSON.parse(response.data); + } + + Vue.activeResources--; }); }); diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index b5a7078a3a1..f880a9862c6 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -37,7 +37,6 @@ class Projects::CommitController < Projects::ApplicationController format.json do render json: PipelineSerializer .new(project: @project, user: @current_user) - .with_pagination(request, response) .represent(@pipelines) end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index deb084c2e91..68f6208c2be 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -218,7 +218,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController format.json do render json: PipelineSerializer .new(project: @project, user: @current_user) - .with_pagination(request, response) .represent(@pipelines) end end diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index c1f48837e0e..e00ae629e4b 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -44,9 +44,9 @@ = render "projects/merge_requests/show/commits" #diffs.diffs.tab-pane -# This tab is always loaded via AJAX - #pipelines.pipelines.tab-pane - //TODO: This needs to make a new request every time is opened! - = render "projects/merge_requests/show/pipelines", endpoint: link_to url_for(params) + - if @pipelines.any? + #pipelines.pipelines.tab-pane + = render "projects/merge_requests/show/pipelines", endpoint: link_to url_for(params) .mr-loading-status = spinner diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 8dfe967a937..f131836058b 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -94,7 +94,6 @@ #commits.commits.tab-pane -# This tab is always loaded via AJAX #pipelines.pipelines.tab-pane - //TODO: This needs to make a new request every time is opened! = render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) #diffs.diffs.tab-pane -# This tab is always loaded via AJAX diff --git a/spec/javascripts/commit/pipelines/mock_data.js.es6 b/spec/javascripts/commit/pipelines/mock_data.js.es6 new file mode 100644 index 00000000000..5f0f26a013c --- /dev/null +++ b/spec/javascripts/commit/pipelines/mock_data.js.es6 @@ -0,0 +1,90 @@ +/* eslint-disable no-unused-vars */ +const pipeline = { + id: 73, + 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/root', + }, + path: '/root/review-app/pipelines/73', + details: { + status: { + icon: 'icon_status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + has_details: true, + details_path: '/root/review-app/pipelines/73', + }, + duration: null, + finished_at: '2017-01-25T00:00:17.130Z', + stages: [{ + name: 'build', + title: 'build: failed', + status: { + icon: 'icon_status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + has_details: true, + details_path: '/root/review-app/pipelines/73#build', + }, + path: '/root/review-app/pipelines/73#build', + dropdown_path: '/root/review-app/pipelines/73/stage.json?stage=build', + }], + artifacts: [], + manual_actions: [ + { + name: 'stop_review', + path: '/root/review-app/builds/1463/play', + }, + { + name: 'name', + path: '/root/review-app/builds/1490/play', + }, + ], + }, + flags: { + latest: true, + triggered: false, + stuck: false, + yaml_errors: false, + retryable: true, + cancelable: false, + }, + ref: + { + name: 'master', + path: '/root/review-app/tree/master', + tag: false, + branch: true, + }, + commit: { + id: 'fbd79f04fa98717641deaaeb092a4d417237c2e4', + short_id: 'fbd79f04', + title: 'Update .gitlab-ci.yml', + author_name: 'Administrator', + author_email: 'admin@example.com', + created_at: '2017-01-16T12:13:57.000-05:00', + committer_name: 'Administrator', + committer_email: 'admin@example.com', + message: 'Update .gitlab-ci.yml', + author: { + 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/root', + }, + author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + commit_url: 'http://localhost:3000/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4', + commit_path: '/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4', + }, + retry_path: '/root/review-app/pipelines/73/retry', + created_at: '2017-01-16T17:13:59.800Z', + updated_at: '2017-01-25T00:00:17.132Z', +}; diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 new file mode 100644 index 00000000000..3bcc0d1eb18 --- /dev/null +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 @@ -0,0 +1,107 @@ +/* global pipeline, Vue */ + +//= require vue +//= require vue-resource +//= require flash +//= require commit/pipelines/pipelines_store +//= require commit/pipelines/pipelines_service +//= require commit/pipelines/pipelines_table +//= require vue_shared/vue_resource_interceptor +//= require ./mock_data + +describe('Pipelines table in Commits and Merge requests', () => { + preloadFixtures('pipelines_table'); + + beforeEach(() => { + loadFixtures('pipelines_table'); + }); + + describe('successfull request', () => { + describe('without pipelines', () => { + const pipelinesEmptyResponse = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(pipelinesEmptyResponse); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, pipelinesEmptyResponse, + ); + }); + + it('should render the empty state', (done) => { + const component = new gl.commits.pipelines.PipelinesTableView({ + el: document.querySelector('#commit-pipeline-table-view'), + }); + + setTimeout(() => { + expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show'); + done(); + }, 1); + }); + }); + + describe('with pipelines', () => { + const pipelinesResponse = (request, next) => { + next(request.respondWith(JSON.stringify([pipeline]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(pipelinesResponse); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, pipelinesResponse, + ); + }); + + it('should render a table with the received pipelines', (done) => { + const component = new gl.commits.pipelines.PipelinesTableView({ + el: document.querySelector('#commit-pipeline-table-view'), + }); + + setTimeout(() => { + expect(component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1); + done(); + }, 0); + }); + }); + }); + + describe('unsuccessfull request', () => { + const pipelinesErrorResponse = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 500, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(pipelinesErrorResponse); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, pipelinesErrorResponse, + ); + }); + + it('should render empty state', (done) => { + const component = new gl.commits.pipelines.PipelinesTableView({ + el: document.querySelector('#commit-pipeline-table-view'), + }); + + setTimeout(() => { + expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show'); + done(); + }, 0); + }); + }); +}); diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 new file mode 100644 index 00000000000..46a7df3bb21 --- /dev/null +++ b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 @@ -0,0 +1,31 @@ +//= require vue +//= require commit/pipelines/pipelines_store + +describe('Store', () => { + const store = gl.commits.pipelines.PipelinesStore; + + beforeEach(() => { + store.create(); + }); + + it('should start with a blank state', () => { + expect(store.state.pipelines.length).toBe(0); + }); + + it('should store an array of pipelines', () => { + const pipelines = [ + { + id: '1', + name: 'pipeline', + }, + { + id: '2', + name: 'pipeline_2', + }, + ]; + + store.store(pipelines); + + expect(store.state.pipelines.length).toBe(pipelines.length); + }); +}); diff --git a/spec/javascripts/fixtures/pipelines_table.html.haml b/spec/javascripts/fixtures/pipelines_table.html.haml new file mode 100644 index 00000000000..fbe4a434f76 --- /dev/null +++ b/spec/javascripts/fixtures/pipelines_table.html.haml @@ -0,0 +1,2 @@ +#commit-pipeline-table-view{ data: { endpoint: "endpoint" } } +.pipeline-svgs{ data: { "commit_icon_svg": "svg"} } diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 new file mode 100644 index 00000000000..6825de069e4 --- /dev/null +++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 @@ -0,0 +1,90 @@ +/* global pipeline */ + +//= require vue +//= require vue_shared/components/pipelines_table_row +//= require commit/pipelines/mock_data + +describe('Pipelines Table Row', () => { + let component; + preloadFixtures('static/environments/element.html.raw'); + + beforeEach(() => { + loadFixtures('static/environments/element.html.raw'); + + component = new gl.pipelines.PipelinesTableRowComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + pipeline, + svgs: {}, + }, + }); + }); + + it('should render a table row', () => { + expect(component.$el).toEqual('TR'); + }); + + describe('status column', () => { + it('should render a pipeline link', () => { + expect( + component.$el.querySelector('td.commit-link a').getAttribute('href'), + ).toEqual(pipeline.path); + }); + + it('should render status text', () => { + expect( + component.$el.querySelector('td.commit-link a').textContent, + ).toContain(pipeline.details.status.text); + }); + }); + + describe('information column', () => { + it('should render a pipeline link', () => { + expect( + component.$el.querySelector('td:nth-child(2) a').getAttribute('href'), + ).toEqual(pipeline.path); + }); + + it('should render pipeline ID', () => { + expect( + component.$el.querySelector('td:nth-child(2) a > span').textContent, + ).toEqual(`#${pipeline.id}`); + }); + + describe('when a user is provided', () => { + it('should render user information', () => { + expect( + component.$el.querySelector('td:nth-child(2) a:nth-child(3)').getAttribute('href'), + ).toEqual(pipeline.user.web_url); + + expect( + component.$el.querySelector('td:nth-child(2) img').getAttribute('title'), + ).toEqual(pipeline.user.name); + }); + }); + }); + + describe('commit column', () => { + it('should render link to commit', () => { + expect( + component.$el.querySelector('td:nth-child(3) .commit-id').getAttribute('href'), + ).toEqual(pipeline.commit.commit_path); + }); + }); + + describe('stages column', () => { + it('should render an icon for each stage', () => { + expect( + component.$el.querySelectorAll('td:nth-child(4) .js-builds-dropdown-button').length, + ).toEqual(pipeline.details.stages.length); + }); + }); + + describe('actions column', () => { + it('should render the provided actions', () => { + expect( + component.$el.querySelectorAll('td:nth-child(6) ul li').length, + ).toEqual(pipeline.details.manual_actions.length); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 new file mode 100644 index 00000000000..cb1006d44dc --- /dev/null +++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 @@ -0,0 +1,67 @@ +/* global pipeline */ + +//= require vue +//= require vue_shared/components/pipelines_table +//= require commit/pipelines/mock_data +//= require lib/utils/datetime_utility + +describe('Pipelines Table', () => { + preloadFixtures('static/environments/element.html.raw'); + + beforeEach(() => { + loadFixtures('static/environments/element.html.raw'); + }); + + describe('table', () => { + let component; + beforeEach(() => { + component = new gl.pipelines.PipelinesTableComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + pipelines: [], + svgs: {}, + }, + }); + }); + + it('should render a table', () => { + expect(component.$el).toEqual('TABLE'); + }); + + it('should render table head with correct columns', () => { + expect(component.$el.querySelector('th.js-pipeline-status').textContent).toEqual('Status'); + expect(component.$el.querySelector('th.js-pipeline-info').textContent).toEqual('Pipeline'); + expect(component.$el.querySelector('th.js-pipeline-commit').textContent).toEqual('Commit'); + expect(component.$el.querySelector('th.js-pipeline-stages').textContent).toEqual('Stages'); + expect(component.$el.querySelector('th.js-pipeline-date').textContent).toEqual(''); + expect(component.$el.querySelector('th.js-pipeline-actions').textContent).toEqual(''); + }); + }); + + describe('without data', () => { + it('should render an empty table', () => { + const component = new gl.pipelines.PipelinesTableComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + pipelines: [], + svgs: {}, + }, + }); + expect(component.$el.querySelectorAll('tbody tr').length).toEqual(0); + }); + }); + + describe('with data', () => { + it('should render rows', () => { + const component = new gl.pipelines.PipelinesTableComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + pipelines: [pipeline], + svgs: {}, + }, + }); + + expect(component.$el.querySelectorAll('tbody tr').length).toEqual(1); + }); + }); +}); diff --git a/spec/javascripts/vue_pagination/pagination_spec.js.es6 b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 similarity index 98% rename from spec/javascripts/vue_pagination/pagination_spec.js.es6 rename to spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 index efb11211ce2..6a0fec43d2e 100644 --- a/spec/javascripts/vue_pagination/pagination_spec.js.es6 +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 @@ -1,6 +1,7 @@ //= require vue //= require lib/utils/common_utils -//= require vue_pagination/index +//= require vue_shared/components/table_pagination +/* global fixture, gl */ describe('Pagination component', () => { let component; From 45966b0abc70986f8dbd1694f8cef23546c81385 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 1 Feb 2017 19:17:58 +0100 Subject: [PATCH 073/488] Fix syntax error in the new merge request view --- app/views/projects/merge_requests/_new_submit.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index e00ae629e4b..38259faf62f 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -46,7 +46,7 @@ -# This tab is always loaded via AJAX - if @pipelines.any? #pipelines.pipelines.tab-pane - = render "projects/merge_requests/show/pipelines", endpoint: link_to url_for(params) + = render "projects/merge_requests/show/pipelines", endpoint: link_to(url_for(params)) .mr-loading-status = spinner From 921141aebdf70161ecd3b2eb9038d271f5a3331c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 1 Feb 2017 19:20:08 +0100 Subject: [PATCH 074/488] Serialize pipelines in the new merge request action --- app/controllers/projects/merge_requests_controller.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 68f6208c2be..38a1946a71e 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -224,7 +224,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def new - define_new_vars + respond_to do |format| + format.html { define_new_vars } + format.json do + render json: { pipelines: PipelineSerializer + .new(project: @project, user: @current_user) + .represent(@pipelines) } + end + end end def new_diffs From 562b5015edaecb09d1237cba7ed820b95ec425f7 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 1 Feb 2017 20:06:11 +0100 Subject: [PATCH 075/488] Add basic specs for new merge requests pipelines API --- .../merge_requests_controller_spec.rb | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index e019541e74f..e100047579d 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -22,23 +22,35 @@ describe Projects::MergeRequestsController do render_views let(:fork_project) { create(:forked_project_with_submodules) } + before { fork_project.team << [user, :master] } - before do - fork_project.team << [user, :master] + context 'when rendering HTML response' do + it 'renders new merge request widget template' do + submit_new_merge_request + + expect(response).to be_success + end end - it 'renders it' do - get :new, - namespace_id: fork_project.namespace.to_param, - project_id: fork_project.to_param, - merge_request: { - source_branch: 'remove-submodule', - target_branch: 'master' - } + context 'when rendering JSON response' do + it 'renders JSON including serialized pipelines' do + submit_new_merge_request(format: :json) - expect(response).to be_success + expect(json_response).to have_key('pipelines') + expect(response).to be_ok + end end end + + def submit_new_merge_request(format: :html) + get :new, + namespace_id: fork_project.namespace.to_param, + project_id: fork_project.to_param, + merge_request: { + source_branch: 'remove-submodule', + target_branch: 'master' }, + format: format + end end shared_examples "loads labels" do |action| From afa929143251e0c0558657899132fa11823a2e57 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 1 Feb 2017 19:53:03 +0000 Subject: [PATCH 076/488] Adds changelog entry --- changelogs/unreleased/fe-commit-mr-pipelines.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fe-commit-mr-pipelines.yml diff --git a/changelogs/unreleased/fe-commit-mr-pipelines.yml b/changelogs/unreleased/fe-commit-mr-pipelines.yml new file mode 100644 index 00000000000..b5cc6bbf8b6 --- /dev/null +++ b/changelogs/unreleased/fe-commit-mr-pipelines.yml @@ -0,0 +1,4 @@ +--- +title: Use vue.js Pipelines table in commit and merge request view +merge_request: 8844 +author: From 035cb734d27cb6df56803d10be408c6e0cf764f0 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 2 Feb 2017 19:43:22 +0000 Subject: [PATCH 077/488] Add time ago auto-update to the 2 newest tables --- .../commit/pipelines/pipelines_store.js.es6 | 28 +++++++++++++++++++ .../commit/pipelines/pipelines_table.js.es6 | 7 +++-- .../pipeline_actions.js.es6 | 4 +-- .../vue_pipelines_index/store.js.es6 | 8 ++++-- app/views/projects/pipelines/index.html.haml | 3 +- .../merge_requests_controller_spec.rb | 9 +----- 6 files changed, 43 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 index b7d8e97fed3..fe90e7bac0a 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable no-underscore-dangle*/ /** * Pipelines' Store for commits view. * @@ -20,7 +21,34 @@ store(pipelines = []) { this.state.pipelines = pipelines; + return pipelines; }, + + /** + * Once the data is received we will start the time ago loops. + * + * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we + * update the time to show how long as passed. + * + */ + startTimeAgoLoops() { + const startTimeLoops = () => { + this.timeLoopInterval = setInterval(() => { + this.$children[0].$children.reduce((acc, component) => { + const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0]; + acc.push(timeAgoComponent); + return acc; + }, []).forEach(e => e.changeTime()); + }, 10000); + }; + + startTimeLoops(); + + const removeIntervals = () => clearInterval(this.timeLoopInterval); + const startIntervals = () => startTimeLoops(); + + gl.VueRealtimeListener(removeIntervals, startIntervals); + }, }; })(); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 index df7a6455eed..18d57333f61 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 @@ -5,6 +5,7 @@ //= require vue-resource //= require vue_shared/vue_resource_interceptor //= require vue_shared/components/pipelines_table +//= require vue_realtime_listener/index /** * @@ -71,10 +72,12 @@ .then(response => response.json()) .then((json) => { this.store.store(json); + this.store.startTimeAgoLoops.call(this, Vue); this.isLoading = false; - }).catch(() => { + }) + .catch(() => { this.isLoading = false; - new Flash('An error occurred while fetching the pipelines.', 'alert'); + new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert'); }); }, diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 index 9b4897b1a9e..e8f91227345 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -70,7 +70,7 @@
-
. Bailing hydration and performing ' + - 'full client-side render.' - ); - } - } - // either not server-rendered, or hydration failed. - // create an empty node and replace it - oldVnode = emptyNodeAt(oldVnode); - } - elm = oldVnode.elm; - parent = nodeOps.parentNode(elm); - - createElm(vnode, insertedVnodeQueue); - - // component root element replaced. - // update parent placeholder node element. - if (vnode.parent) { - vnode.parent.elm = vnode.elm; - if (isPatchable(vnode)) { - for (var i = 0; i < cbs.create.length; ++i) { - cbs.create[i](emptyNode, vnode.parent); - } - } - } - - if (parent !== null) { - nodeOps.insertBefore(parent, vnode.elm, nodeOps.nextSibling(elm)); - removeVnodes(parent, [oldVnode], 0, 0); - } else if (isDef(oldVnode.tag)) { - invokeDestroyHook(oldVnode); - } - } - } - - invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch); - return vnode.elm - } -} - -/* */ - -var directives = { - create: updateDirectives, - update: updateDirectives, - destroy: function unbindDirectives (vnode) { - updateDirectives(vnode, emptyNode); - } -}; - -function updateDirectives ( - oldVnode, - vnode -) { - if (!oldVnode.data.directives && !vnode.data.directives) { - return - } - var isCreate = oldVnode === emptyNode; - var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context); - var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context); - - var dirsWithInsert = []; - var dirsWithPostpatch = []; - - var key, oldDir, dir; - for (key in newDirs) { - oldDir = oldDirs[key]; - dir = newDirs[key]; - if (!oldDir) { - // new directive, bind - callHook$1(dir, 'bind', vnode, oldVnode); - if (dir.def && dir.def.inserted) { - dirsWithInsert.push(dir); - } - } else { - // existing directive, update - dir.oldValue = oldDir.value; - callHook$1(dir, 'update', vnode, oldVnode); - if (dir.def && dir.def.componentUpdated) { - dirsWithPostpatch.push(dir); - } - } - } - - if (dirsWithInsert.length) { - var callInsert = function () { - dirsWithInsert.forEach(function (dir) { - callHook$1(dir, 'inserted', vnode, oldVnode); - }); - }; - if (isCreate) { - mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'insert', callInsert, 'dir-insert'); - } else { - callInsert(); - } - } - - if (dirsWithPostpatch.length) { - mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'postpatch', function () { - dirsWithPostpatch.forEach(function (dir) { - callHook$1(dir, 'componentUpdated', vnode, oldVnode); - }); - }, 'dir-postpatch'); - } - - if (!isCreate) { - for (key in oldDirs) { - if (!newDirs[key]) { - // no longer present, unbind - callHook$1(oldDirs[key], 'unbind', oldVnode); - } - } - } -} - -var emptyModifiers = Object.create(null); - -function normalizeDirectives$1 ( - dirs, - vm -) { - var res = Object.create(null); - if (!dirs) { - return res - } - var i, dir; - for (i = 0; i < dirs.length; i++) { - dir = dirs[i]; - if (!dir.modifiers) { - dir.modifiers = emptyModifiers; - } - res[getRawDirName(dir)] = dir; - dir.def = resolveAsset(vm.$options, 'directives', dir.name, true); - } - return res -} - -function getRawDirName (dir) { - return dir.rawName || ((dir.name) + "." + (Object.keys(dir.modifiers || {}).join('.'))) -} - -function callHook$1 (dir, hook, vnode, oldVnode) { - var fn = dir.def && dir.def[hook]; - if (fn) { - fn(vnode.elm, dir, vnode, oldVnode); - } -} - -var baseModules = [ - ref, - directives -]; - -/* */ - -function updateAttrs (oldVnode, vnode) { - if (!oldVnode.data.attrs && !vnode.data.attrs) { - return - } - var key, cur, old; - var elm = vnode.elm; - var oldAttrs = oldVnode.data.attrs || {}; - var attrs = vnode.data.attrs || {}; - // clone observed objects, as the user probably wants to mutate it - if (attrs.__ob__) { - attrs = vnode.data.attrs = extend({}, attrs); - } - - for (key in attrs) { - cur = attrs[key]; - old = oldAttrs[key]; - if (old !== cur) { - setAttr(elm, key, cur); - } - } - for (key in oldAttrs) { - if (attrs[key] == null) { - if (isXlink(key)) { - elm.removeAttributeNS(xlinkNS, getXlinkProp(key)); - } else if (!isEnumeratedAttr(key)) { - elm.removeAttribute(key); - } - } - } -} - -function setAttr (el, key, value) { - if (isBooleanAttr(key)) { - // set attribute for blank value - // e.g. - if (isFalsyAttrValue(value)) { - el.removeAttribute(key); - } else { - el.setAttribute(key, key); - } - } else if (isEnumeratedAttr(key)) { - el.setAttribute(key, isFalsyAttrValue(value) || value === 'false' ? 'false' : 'true'); - } else if (isXlink(key)) { - if (isFalsyAttrValue(value)) { - el.removeAttributeNS(xlinkNS, getXlinkProp(key)); - } else { - el.setAttributeNS(xlinkNS, key, value); - } - } else { - if (isFalsyAttrValue(value)) { - el.removeAttribute(key); - } else { - el.setAttribute(key, value); - } - } -} - -var attrs = { - create: updateAttrs, - update: updateAttrs -}; - -/* */ - -function updateClass (oldVnode, vnode) { - var el = vnode.elm; - var data = vnode.data; - var oldData = oldVnode.data; - if (!data.staticClass && !data.class && - (!oldData || (!oldData.staticClass && !oldData.class))) { - return - } - - var cls = genClassForVnode(vnode); - - // handle transition classes - var transitionClass = el._transitionClasses; - if (transitionClass) { - cls = concat(cls, stringifyClass(transitionClass)); - } - - // set the class - if (cls !== el._prevClass) { - el.setAttribute('class', cls); - el._prevClass = cls; - } -} - -var klass = { - create: updateClass, - update: updateClass -}; - -// skip type checking this file because we need to attach private properties -// to elements - -function updateDOMListeners (oldVnode, vnode) { - if (!oldVnode.data.on && !vnode.data.on) { - return - } - var on = vnode.data.on || {}; - var oldOn = oldVnode.data.on || {}; - var add = vnode.elm._v_add || (vnode.elm._v_add = function (event, handler, capture) { - vnode.elm.addEventListener(event, handler, capture); - }); - var remove = vnode.elm._v_remove || (vnode.elm._v_remove = function (event, handler) { - vnode.elm.removeEventListener(event, handler); - }); - updateListeners(on, oldOn, add, remove, vnode.context); -} - -var events = { - create: updateDOMListeners, - update: updateDOMListeners -}; - -/* */ - -function updateDOMProps (oldVnode, vnode) { - if (!oldVnode.data.domProps && !vnode.data.domProps) { - return - } - var key, cur; - var elm = vnode.elm; - var oldProps = oldVnode.data.domProps || {}; - var props = vnode.data.domProps || {}; - // clone observed objects, as the user probably wants to mutate it - if (props.__ob__) { - props = vnode.data.domProps = extend({}, props); - } - - for (key in oldProps) { - if (props[key] == null) { - elm[key] = undefined; - } - } - for (key in props) { - // ignore children if the node has textContent or innerHTML, - // as these will throw away existing DOM nodes and cause removal errors - // on subsequent patches (#3360) - if ((key === 'textContent' || key === 'innerHTML') && vnode.children) { - vnode.children.length = 0; - } - cur = props[key]; - if (key === 'value') { - // store value as _value as well since - // non-string values will be stringified - elm._value = cur; - // avoid resetting cursor position when value is the same - var strCur = cur == null ? '' : String(cur); - if (elm.value !== strCur && !elm.composing) { - elm.value = strCur; - } - } else { - elm[key] = cur; - } - } -} - -var domProps = { - create: updateDOMProps, - update: updateDOMProps -}; - -/* */ - -var prefixes = ['Webkit', 'Moz', 'ms']; - -var testEl; -var normalize = cached(function (prop) { - testEl = testEl || document.createElement('div'); - prop = camelize(prop); - if (prop !== 'filter' && (prop in testEl.style)) { - return prop - } - var upper = prop.charAt(0).toUpperCase() + prop.slice(1); - for (var i = 0; i < prefixes.length; i++) { - var prefixed = prefixes[i] + upper; - if (prefixed in testEl.style) { - return prefixed - } - } -}); - -function updateStyle (oldVnode, vnode) { - if ((!oldVnode.data || !oldVnode.data.style) && !vnode.data.style) { - return - } - var cur, name; - var el = vnode.elm; - var oldStyle = oldVnode.data.style || {}; - var style = vnode.data.style || {}; - - // handle string - if (typeof style === 'string') { - el.style.cssText = style; - return - } - - var needClone = style.__ob__; - - // handle array syntax - if (Array.isArray(style)) { - style = vnode.data.style = toObject(style); - } - - // clone the style for future updates, - // in case the user mutates the style object in-place. - if (needClone) { - style = vnode.data.style = extend({}, style); - } - - for (name in oldStyle) { - if (style[name] == null) { - el.style[normalize(name)] = ''; - } - } - for (name in style) { - cur = style[name]; - if (cur !== oldStyle[name]) { - // ie9 setting to null has no effect, must use empty string - el.style[normalize(name)] = cur == null ? '' : cur; - } - } -} - -var style = { - create: updateStyle, - update: updateStyle -}; - -/* */ - -/** - * Add class with compatibility for SVG since classList is not supported on - * SVG elements in IE - */ -function addClass (el, cls) { - /* istanbul ignore else */ - if (el.classList) { - if (cls.indexOf(' ') > -1) { - cls.split(/\s+/).forEach(function (c) { return el.classList.add(c); }); - } else { - el.classList.add(cls); - } - } else { - var cur = ' ' + el.getAttribute('class') + ' '; - if (cur.indexOf(' ' + cls + ' ') < 0) { - el.setAttribute('class', (cur + cls).trim()); - } - } -} - -/** - * Remove class with compatibility for SVG since classList is not supported on - * SVG elements in IE - */ -function removeClass (el, cls) { - /* istanbul ignore else */ - if (el.classList) { - if (cls.indexOf(' ') > -1) { - cls.split(/\s+/).forEach(function (c) { return el.classList.remove(c); }); - } else { - el.classList.remove(cls); - } - } else { - var cur = ' ' + el.getAttribute('class') + ' '; - var tar = ' ' + cls + ' '; - while (cur.indexOf(tar) >= 0) { - cur = cur.replace(tar, ' '); - } - el.setAttribute('class', cur.trim()); - } -} - -/* */ - -var hasTransition = inBrowser && !isIE9; -var TRANSITION = 'transition'; -var ANIMATION = 'animation'; - -// Transition property/event sniffing -var transitionProp = 'transition'; -var transitionEndEvent = 'transitionend'; -var animationProp = 'animation'; -var animationEndEvent = 'animationend'; -if (hasTransition) { - /* istanbul ignore if */ - if (window.ontransitionend === undefined && - window.onwebkittransitionend !== undefined) { - transitionProp = 'WebkitTransition'; - transitionEndEvent = 'webkitTransitionEnd'; - } - if (window.onanimationend === undefined && - window.onwebkitanimationend !== undefined) { - animationProp = 'WebkitAnimation'; - animationEndEvent = 'webkitAnimationEnd'; - } -} - -var raf = (inBrowser && window.requestAnimationFrame) || setTimeout; -function nextFrame (fn) { - raf(function () { - raf(fn); - }); -} - -function addTransitionClass (el, cls) { - (el._transitionClasses || (el._transitionClasses = [])).push(cls); - addClass(el, cls); -} - -function removeTransitionClass (el, cls) { - if (el._transitionClasses) { - remove$1(el._transitionClasses, cls); - } - removeClass(el, cls); -} - -function whenTransitionEnds ( - el, - expectedType, - cb -) { - var ref = getTransitionInfo(el, expectedType); - var type = ref.type; - var timeout = ref.timeout; - var propCount = ref.propCount; - if (!type) { return cb() } - var event = type === TRANSITION ? transitionEndEvent : animationEndEvent; - var ended = 0; - var end = function () { - el.removeEventListener(event, onEnd); - cb(); - }; - var onEnd = function (e) { - if (e.target === el) { - if (++ended >= propCount) { - end(); - } - } - }; - setTimeout(function () { - if (ended < propCount) { - end(); - } - }, timeout + 1); - el.addEventListener(event, onEnd); -} - -var transformRE = /\b(transform|all)(,|$)/; - -function getTransitionInfo (el, expectedType) { - var styles = window.getComputedStyle(el); - var transitioneDelays = styles[transitionProp + 'Delay'].split(', '); - var transitionDurations = styles[transitionProp + 'Duration'].split(', '); - var transitionTimeout = getTimeout(transitioneDelays, transitionDurations); - var animationDelays = styles[animationProp + 'Delay'].split(', '); - var animationDurations = styles[animationProp + 'Duration'].split(', '); - var animationTimeout = getTimeout(animationDelays, animationDurations); - - var type; - var timeout = 0; - var propCount = 0; - /* istanbul ignore if */ - if (expectedType === TRANSITION) { - if (transitionTimeout > 0) { - type = TRANSITION; - timeout = transitionTimeout; - propCount = transitionDurations.length; - } - } else if (expectedType === ANIMATION) { - if (animationTimeout > 0) { - type = ANIMATION; - timeout = animationTimeout; - propCount = animationDurations.length; - } - } else { - timeout = Math.max(transitionTimeout, animationTimeout); - type = timeout > 0 - ? transitionTimeout > animationTimeout - ? TRANSITION - : ANIMATION - : null; - propCount = type - ? type === TRANSITION - ? transitionDurations.length - : animationDurations.length - : 0; - } - var hasTransform = - type === TRANSITION && - transformRE.test(styles[transitionProp + 'Property']); - return { - type: type, - timeout: timeout, - propCount: propCount, - hasTransform: hasTransform - } -} - -function getTimeout (delays, durations) { - return Math.max.apply(null, durations.map(function (d, i) { - return toMs(d) + toMs(delays[i]) - })) -} - -function toMs (s) { - return Number(s.slice(0, -1)) * 1000 -} - -/* */ - -function enter (vnode) { - var el = vnode.elm; - - // call leave callback now - if (el._leaveCb) { - el._leaveCb.cancelled = true; - el._leaveCb(); - } - - var data = resolveTransition(vnode.data.transition); - if (!data) { - return - } - - /* istanbul ignore if */ - if (el._enterCb || el.nodeType !== 1) { - return - } - - var css = data.css; - var type = data.type; - var enterClass = data.enterClass; - var enterActiveClass = data.enterActiveClass; - var appearClass = data.appearClass; - var appearActiveClass = data.appearActiveClass; - var beforeEnter = data.beforeEnter; - var enter = data.enter; - var afterEnter = data.afterEnter; - var enterCancelled = data.enterCancelled; - var beforeAppear = data.beforeAppear; - var appear = data.appear; - var afterAppear = data.afterAppear; - var appearCancelled = data.appearCancelled; - - // activeInstance will always be the component managing this - // transition. One edge case to check is when the is placed - // as the root node of a child component. In that case we need to check - // 's parent for appear check. - var transitionNode = activeInstance.$vnode; - var context = transitionNode && transitionNode.parent - ? transitionNode.parent.context - : activeInstance; - - var isAppear = !context._isMounted || !vnode.isRootInsert; - - if (isAppear && !appear && appear !== '') { - return - } - - var startClass = isAppear ? appearClass : enterClass; - var activeClass = isAppear ? appearActiveClass : enterActiveClass; - var beforeEnterHook = isAppear ? (beforeAppear || beforeEnter) : beforeEnter; - var enterHook = isAppear ? (typeof appear === 'function' ? appear : enter) : enter; - var afterEnterHook = isAppear ? (afterAppear || afterEnter) : afterEnter; - var enterCancelledHook = isAppear ? (appearCancelled || enterCancelled) : enterCancelled; - - var expectsCSS = css !== false && !isIE9; - var userWantsControl = - enterHook && - // enterHook may be a bound method which exposes - // the length of original fn as _length - (enterHook._length || enterHook.length) > 1; - - var cb = el._enterCb = once(function () { - if (expectsCSS) { - removeTransitionClass(el, activeClass); - } - if (cb.cancelled) { - if (expectsCSS) { - removeTransitionClass(el, startClass); - } - enterCancelledHook && enterCancelledHook(el); - } else { - afterEnterHook && afterEnterHook(el); - } - el._enterCb = null; - }); - - if (!vnode.data.show) { - // remove pending leave element on enter by injecting an insert hook - mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'insert', function () { - var parent = el.parentNode; - var pendingNode = parent && parent._pending && parent._pending[vnode.key]; - if (pendingNode && pendingNode.tag === vnode.tag && pendingNode.elm._leaveCb) { - pendingNode.elm._leaveCb(); - } - enterHook && enterHook(el, cb); - }, 'transition-insert'); - } - - // start enter transition - beforeEnterHook && beforeEnterHook(el); - if (expectsCSS) { - addTransitionClass(el, startClass); - addTransitionClass(el, activeClass); - nextFrame(function () { - removeTransitionClass(el, startClass); - if (!cb.cancelled && !userWantsControl) { - whenTransitionEnds(el, type, cb); - } - }); - } - - if (vnode.data.show) { - enterHook && enterHook(el, cb); - } - - if (!expectsCSS && !userWantsControl) { - cb(); - } -} - -function leave (vnode, rm) { - var el = vnode.elm; - - // call enter callback now - if (el._enterCb) { - el._enterCb.cancelled = true; - el._enterCb(); - } - - var data = resolveTransition(vnode.data.transition); - if (!data) { - return rm() - } - - /* istanbul ignore if */ - if (el._leaveCb || el.nodeType !== 1) { - return - } - - var css = data.css; - var type = data.type; - var leaveClass = data.leaveClass; - var leaveActiveClass = data.leaveActiveClass; - var beforeLeave = data.beforeLeave; - var leave = data.leave; - var afterLeave = data.afterLeave; - var leaveCancelled = data.leaveCancelled; - var delayLeave = data.delayLeave; - - var expectsCSS = css !== false && !isIE9; - var userWantsControl = - leave && - // leave hook may be a bound method which exposes - // the length of original fn as _length - (leave._length || leave.length) > 1; - - var cb = el._leaveCb = once(function () { - if (el.parentNode && el.parentNode._pending) { - el.parentNode._pending[vnode.key] = null; - } - if (expectsCSS) { - removeTransitionClass(el, leaveActiveClass); - } - if (cb.cancelled) { - if (expectsCSS) { - removeTransitionClass(el, leaveClass); - } - leaveCancelled && leaveCancelled(el); - } else { - rm(); - afterLeave && afterLeave(el); - } - el._leaveCb = null; - }); - - if (delayLeave) { - delayLeave(performLeave); - } else { - performLeave(); - } - - function performLeave () { - // the delayed leave may have already been cancelled - if (cb.cancelled) { - return - } - // record leaving element - if (!vnode.data.show) { - (el.parentNode._pending || (el.parentNode._pending = {}))[vnode.key] = vnode; - } - beforeLeave && beforeLeave(el); - if (expectsCSS) { - addTransitionClass(el, leaveClass); - addTransitionClass(el, leaveActiveClass); - nextFrame(function () { - removeTransitionClass(el, leaveClass); - if (!cb.cancelled && !userWantsControl) { - whenTransitionEnds(el, type, cb); - } - }); - } - leave && leave(el, cb); - if (!expectsCSS && !userWantsControl) { - cb(); - } - } -} - -function resolveTransition (def$$1) { - if (!def$$1) { - return - } - /* istanbul ignore else */ - if (typeof def$$1 === 'object') { - var res = {}; - if (def$$1.css !== false) { - extend(res, autoCssTransition(def$$1.name || 'v')); - } - extend(res, def$$1); - return res - } else if (typeof def$$1 === 'string') { - return autoCssTransition(def$$1) - } -} - -var autoCssTransition = cached(function (name) { - return { - enterClass: (name + "-enter"), - leaveClass: (name + "-leave"), - appearClass: (name + "-enter"), - enterActiveClass: (name + "-enter-active"), - leaveActiveClass: (name + "-leave-active"), - appearActiveClass: (name + "-enter-active") - } -}); - -function once (fn) { - var called = false; - return function () { - if (!called) { - called = true; - fn(); - } - } -} - -var transition = inBrowser ? { - create: function create (_, vnode) { - if (!vnode.data.show) { - enter(vnode); - } - }, - remove: function remove (vnode, rm) { - /* istanbul ignore else */ - if (!vnode.data.show) { - leave(vnode, rm); - } else { - rm(); - } - } -} : {}; - -var platformModules = [ - attrs, - klass, - events, - domProps, - style, - transition -]; - -/* */ - -// the directive module should be applied last, after all -// built-in modules have been applied. -var modules = platformModules.concat(baseModules); - -var patch$1 = createPatchFunction({ nodeOps: nodeOps, modules: modules }); - -/** - * Not type checking this file because flow doesn't like attaching - * properties to Elements. - */ - -var modelableTagRE = /^input|select|textarea|vue-component-[0-9]+(-[0-9a-zA-Z_\-]*)?$/; - -/* istanbul ignore if */ -if (isIE9) { - // http://www.matts411.com/post/internet-explorer-9-oninput/ - document.addEventListener('selectionchange', function () { - var el = document.activeElement; - if (el && el.vmodel) { - trigger(el, 'input'); - } - }); -} - -var model = { - inserted: function inserted (el, binding, vnode) { - { - if (!modelableTagRE.test(vnode.tag)) { - warn( - "v-model is not supported on element type: <" + (vnode.tag) + ">. " + - 'If you are working with contenteditable, it\'s recommended to ' + - 'wrap a library dedicated for that purpose inside a custom component.', - vnode.context - ); - } - } - if (vnode.tag === 'select') { - var cb = function () { - setSelected(el, binding, vnode.context); - }; - cb(); - /* istanbul ignore if */ - if (isIE || isEdge) { - setTimeout(cb, 0); - } - } else if ( - (vnode.tag === 'textarea' || el.type === 'text') && - !binding.modifiers.lazy - ) { - if (!isAndroid) { - el.addEventListener('compositionstart', onCompositionStart); - el.addEventListener('compositionend', onCompositionEnd); - } - /* istanbul ignore if */ - if (isIE9) { - el.vmodel = true; - } - } - }, - componentUpdated: function componentUpdated (el, binding, vnode) { - if (vnode.tag === 'select') { - setSelected(el, binding, vnode.context); - // in case the options rendered by v-for have changed, - // it's possible that the value is out-of-sync with the rendered options. - // detect such cases and filter out values that no longer has a matchig - // option in the DOM. - var needReset = el.multiple - ? binding.value.some(function (v) { return hasNoMatchingOption(v, el.options); }) - : binding.value !== binding.oldValue && hasNoMatchingOption(binding.value, el.options); - if (needReset) { - trigger(el, 'change'); - } - } - } -}; - -function setSelected (el, binding, vm) { - var value = binding.value; - var isMultiple = el.multiple; - if (isMultiple && !Array.isArray(value)) { - "development" !== 'production' && warn( - "
StatusPipelineCommitStagesStatusPipelineCommitStages
-
iWH)ULWEG{rO1I-Cl_EplI?n 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

VJ(k%7 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

D7MhLsej>v1V0rHdh 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%0Ds`g2;eDsSd-!G`3j#rwmo8N0emZ5d4hb zh+;ZGKv4%Mj$;ZZNdxjuuH+K+>vTrqT@_v9?}leGr}D30ljH7|P~$t8d3Rv$X?w|_ zT;f@8qc;{F&~}kMs7~u7l6mbOWpBK0@j($hk@Ptg2Ym9Fs{Lrka0L+yEQC zLe$?V8~hi37N?oX{9074ecE51RoXF~xM{sHk7wk?00dT-Bc(bud&HY=w^yu$j($R6 z{#c@wYQ2X%2=8GL{#GVPEIo4&dFJT0ash-qJ1C4$(G?h2a)#N6=mk0eBKqn?E%rC;`X?Ic4e|@R??gzH`Zws_^A~jApfC2%!_glY zJdO$gWry%=w7~jXCjP>|?|wn|9k>!&0zhffL8GIi<}P4grF=?Y3eS0OzYPlhgASiz zvd*`-)8P1l^BY6>(P&KZ4c<=! zO5fg#&ISJKn6JnRbXPzkDJR`A-4b`Z*>Gj;|2jk-wMpfJO#Y@+Uz)B=Qvpn7glqND zUPNYXZS5BxFDVII`W1W^=hb!Hx2l8$I2SC ze_2#32Oq`?Q@b3tsQZ6F%YZI+(7}sB9U;zf13F}$gy$x6)bxDSmW~-4S&0k z|CX8b2h+WKeo5R#-W|-rG Date: Thu, 9 Feb 2017 14:36:03 +0000 Subject: [PATCH 318/488] Documented removing issue --- .../project/img/issue_boards_remove_issue.png | Bin 0 -> 135168 bytes doc/user/project/issue_board.md | 8 ++++++++ 2 files changed, 8 insertions(+) create mode 100644 doc/user/project/img/issue_boards_remove_issue.png diff --git a/doc/user/project/img/issue_boards_remove_issue.png b/doc/user/project/img/issue_boards_remove_issue.png new file mode 100644 index 0000000000000000000000000000000000000000..8b3beca97cf51bfc9dde9bf3a66a6647e3a327cb GIT binary patch literal 135168 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%0DqK2a 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 V(qtw+n}mUWq{S7)%0vwO{|}!GW_|zw literal 0 HcmV?d00001 diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 7738f504922..e7584bc1b83 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -107,6 +107,14 @@ You can filter by author, assignee, milestone and label. ![Bulk adding issues to lists](img/issue_boards_add_issues_modal.png) +## Removing issue from list + +Removing an issue from a list can be done by clicking the issue card and then +clicking the **Remove from board** in the sidebar. Doing this keeps the issue +open buts removes it from that list. + +![Remove issue from list](img/issue_boards_remove_issue.png) + ## Filtering issues You should be able to use the filters on top of your Issue Board to show only From 83e2bfbae04385e09dae924629ac6a9e22b3ad4c Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 9 Feb 2017 09:00:54 -0600 Subject: [PATCH 319/488] Fix expand_collapse_diffs specs --- spec/features/expand_collapse_diffs_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb index 630467e647f..8c64b050e19 100644 --- a/spec/features/expand_collapse_diffs_spec.rb +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -117,7 +117,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do # Wait for diffs find('.js-file-title', match: :first) # Click `large_diff.md` title - all('.js-file-title')[1].click + all('.diff-toggle-caret')[1].click wait_for_ajax end @@ -161,7 +161,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do # Wait for diffs find('.js-file-title', match: :first) # Click `large_diff.md` title - all('.js-file-title')[1].click + all('.diff-toggle-caret')[1].click wait_for_ajax end @@ -183,7 +183,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do # Wait for diffs find('.js-file-title', match: :first) # Click `small_diff.md` title - all('.js-file-title')[3].click + all('.diff-toggle-caret')[3].click end it 'hides the diff content' do @@ -196,7 +196,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do # Wait for diffs find('.js-file-title', match: :first) # Click `small_diff.md` title - all('.js-file-title')[3].click + all('.diff-toggle-caret')[3].click end it 'shows the diff content' do @@ -292,7 +292,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do # Wait for diffs find('.js-file-title', match: :first) # Click `small_diff.md` title - all('.js-file-title')[3].click + all('.diff-toggle-caret')[3].click end it 'hides the diff content' do @@ -305,7 +305,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do # Wait for diffs find('.js-file-title', match: :first) # Click `small_diff.md` title - all('.js-file-title')[3].click + all('.diff-toggle-caret')[3].click end it 'shows the diff content' do From c601c8a3a46eb530e2ca6472ee5fdcda42f3090f Mon Sep 17 00:00:00 2001 From: Robert Marcano Date: Thu, 9 Feb 2017 08:25:46 -0400 Subject: [PATCH 320/488] Removed duplicate "Visibility Level" label on New Project page --- app/views/projects/new.html.haml | 5 ++--- app/views/shared/_visibility_level.html.haml | 11 +++++++---- changelogs/unreleased/issue-newproj-layout.yml | 4 ++++ 3 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/issue-newproj-layout.yml diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index cd685f7d0eb..41473fae4de 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -94,9 +94,8 @@ .form-group.project-visibility-level-holder = f.label :visibility_level, class: 'label-light' do Visibility Level - = link_to "(?)", help_page_path("public_access/public_access") - = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project - + = link_to icon('question-circle'), help_page_path("public_access/public_access") + = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project, with_label: false = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml index b11257ee0e6..73efec88bb1 100644 --- a/app/views/shared/_visibility_level.html.haml +++ b/app/views/shared/_visibility_level.html.haml @@ -1,8 +1,11 @@ +- with_label = local_assigns.fetch(:with_label, true) + .form-group.project-visibility-level-holder - = f.label :visibility_level, class: 'control-label' do - Visibility Level - = link_to icon('question-circle'), help_page_path("public_access/public_access") - .col-sm-10 + - if with_label + = f.label :visibility_level, class: 'control-label' do + Visibility Level + = link_to icon('question-circle'), help_page_path("public_access/public_access") + %div{ :class => ("col-sm-10" if with_label) } - if can_change_visibility_level = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model) - else diff --git a/changelogs/unreleased/issue-newproj-layout.yml b/changelogs/unreleased/issue-newproj-layout.yml new file mode 100644 index 00000000000..d15e8b7d1e5 --- /dev/null +++ b/changelogs/unreleased/issue-newproj-layout.yml @@ -0,0 +1,4 @@ +--- +title: Removed duplicate "Visibility Level" label on New Project page +merge_request: +author: Robert Marcano From f9c58d938851ca63b8c4ff5d2bf9324aa6541170 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 8 Feb 2017 14:58:08 +0000 Subject: [PATCH 321/488] Refactored diff notes Vue app It now relies on its Vue bundle rather than the window. Fixes some reactivity issues that was happening in EE --- .../components/comment_resolve_btn.js.es6 | 11 ++-- .../components/jump_to_discussion.js.es6 | 11 ++-- .../diff_notes/components/resolve_btn.js.es6 | 19 +++---- .../components/resolve_count.js.es6 | 2 +- .../components/resolve_discussion_btn.js.es6 | 13 ++--- .../diff_notes/diff_notes_bundle.js.es6 | 4 ++ .../diff_notes/services/resolve.js.es6 | 56 +++++++++---------- app/assets/javascripts/notes.js | 2 +- app/views/discussions/_resolve_all.html.haml | 3 +- .../projects/merge_requests/_show.html.haml | 3 +- app/views/projects/notes/_note.html.haml | 3 +- 11 files changed, 59 insertions(+), 68 deletions(-) diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 index 2514459e65e..d948dff58ec 100644 --- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 @@ -1,6 +1,6 @@ /* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, max-len */ -/* global Vue */ /* global CommentsStore */ +const Vue = require('vue'); (() => { const CommentAndResolveBtn = Vue.extend({ @@ -9,13 +9,11 @@ }, data() { return { - textareaIsEmpty: true + textareaIsEmpty: true, + discussion: {}, }; }, computed: { - discussion: function () { - return CommentsStore.state[this.discussionId]; - }, showButton: function () { if (this.discussion) { return this.discussion.isResolvable(); @@ -42,6 +40,9 @@ } } }, + created() { + this.discussion = CommentsStore.state[this.discussionId]; + }, mounted: function () { const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`); this.textareaIsEmpty = $textarea.val() === ''; diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 index c3898873eaa..57cb0d0ae6e 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 @@ -1,7 +1,7 @@ /* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, space-before-function-paren, no-lonely-if, no-continue, brace-style, max-len, quotes */ -/* global Vue */ /* global DiscussionMixins */ /* global CommentsStore */ +const Vue = require('vue'); (() => { const JumpToDiscussion = Vue.extend({ @@ -12,12 +12,10 @@ data: function () { return { discussions: CommentsStore.state, + discussion: {}, }; }, computed: { - discussion: function () { - return this.discussions[this.discussionId]; - }, allResolved: function () { return this.unresolvedDiscussionCount === 0; }, @@ -186,7 +184,10 @@ offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()) }); } - } + }, + created() { + this.discussion = this.discussions[this.discussionId]; + }, }); Vue.component('jump-to-discussion', JumpToDiscussion); diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 index 5852b8bbdb7..d1873d6c7a2 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 @@ -1,8 +1,8 @@ /* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */ -/* global Vue */ /* global CommentsStore */ /* global ResolveService */ /* global Flash */ +const Vue = require('vue'); (() => { const ResolveBtn = Vue.extend({ @@ -10,14 +10,14 @@ noteId: Number, discussionId: String, resolved: Boolean, - projectPath: String, canResolve: Boolean, resolvedBy: String }, data: function () { return { discussions: CommentsStore.state, - loading: false + loading: false, + note: {}, }; }, watch: { @@ -30,13 +30,6 @@ discussion: function () { return this.discussions[this.discussionId]; }, - note: function () { - if (this.discussion) { - return this.discussion.getNote(this.noteId); - } else { - return undefined; - } - }, buttonText: function () { if (this.isResolved) { return `Resolved by ${this.resolvedByName}`; @@ -73,10 +66,10 @@ if (this.isResolved) { promise = ResolveService - .unresolve(this.projectPath, this.noteId); + .unresolve(this.noteId); } else { promise = ResolveService - .resolve(this.projectPath, this.noteId); + .resolve(this.noteId); } promise.then((response) => { @@ -106,6 +99,8 @@ }, created: function () { CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy); + + this.note = this.discussion.getNote(this.noteId); } }); diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 index 72cdae812bc..de9367f2136 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 @@ -1,7 +1,7 @@ /* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */ -/* global Vue */ /* global DiscussionMixins */ /* global CommentsStore */ +const Vue = require('vue'); ((w) => { w.ResolveCount = Vue.extend({ diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 index ee5f62b2d9e..7c5fcd04d2d 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 @@ -1,25 +1,22 @@ /* eslint-disable object-shorthand, func-names, space-before-function-paren, comma-dangle, no-else-return, quotes, max-len */ -/* global Vue */ /* global CommentsStore */ /* global ResolveService */ +const Vue = require('vue'); + (() => { const ResolveDiscussionBtn = Vue.extend({ props: { discussionId: String, mergeRequestId: Number, - projectPath: String, canResolve: Boolean, }, data: function() { return { - discussions: CommentsStore.state + discussion: {}, }; }, computed: { - discussion: function () { - return this.discussions[this.discussionId]; - }, showButton: function () { if (this.discussion) { return this.discussion.isResolvable(); @@ -51,11 +48,13 @@ }, methods: { resolve: function () { - ResolveService.toggleResolveForDiscussion(this.projectPath, this.mergeRequestId, this.discussionId); + ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId); } }, created: function () { CommentsStore.createDiscussion(this.discussionId, this.canResolve); + + this.discussion = CommentsStore.state[this.discussionId]; } }); diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 index f0edfb8aaf1..190461451d5 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 @@ -3,6 +3,7 @@ /* global ResolveCount */ function requireAll(context) { return context.keys().map(context); } +const Vue = require('vue'); requireAll(require.context('./models', false, /^\.\/.*\.(js|es6)$/)); requireAll(require.context('./stores', false, /^\.\/.*\.(js|es6)$/)); requireAll(require.context('./services', false, /^\.\/.*\.(js|es6)$/)); @@ -10,11 +11,14 @@ requireAll(require.context('./mixins', false, /^\.\/.*\.(js|es6)$/)); requireAll(require.context('./components', false, /^\.\/.*\.(js|es6)$/)); $(() => { + const projectPath = document.querySelector('.merge-request').dataset.projectPath; const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn'; window.gl = window.gl || {}; window.gl.diffNoteApps = {}; + window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath); + gl.diffNotesCompileComponents = () => { const $components = $(COMPONENT_SELECTOR).filter(function () { return $(this).closest('resolve-count').length !== 1; diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6 index a52c476352d..d83a44ee205 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js.es6 +++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6 @@ -1,45 +1,43 @@ /* eslint-disable class-methods-use-this, one-var, camelcase, no-new, comma-dangle, no-param-reassign, max-len */ -/* global Vue */ /* global Flash */ /* global CommentsStore */ -((w) => { +const Vue = window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); + +(() => { + window.gl = window.gl || {}; + class ResolveServiceClass { - constructor() { - this.noteResource = Vue.resource('notes{/noteId}/resolve'); - this.discussionResource = Vue.resource('merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve'); + constructor(root) { + this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve`); + this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`); + + Vue.http.interceptors.push((request, next) => { + if ($.rails) { + request.headers['X-CSRF-Token'] = $.rails.csrfToken(); + } + next(); + }); } - setCSRF() { - Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken(); - } - - prepareRequest(root) { - this.setCSRF(); - Vue.http.options.root = root; - } - - resolve(projectPath, noteId) { - this.prepareRequest(projectPath); - + resolve(noteId) { return this.noteResource.save({ noteId }, {}); } - unresolve(projectPath, noteId) { - this.prepareRequest(projectPath); - + unresolve(noteId) { return this.noteResource.delete({ noteId }, {}); } - toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId) { + toggleResolveForDiscussion(mergeRequestId, discussionId) { const discussion = CommentsStore.state[discussionId]; const isResolved = discussion.isResolved(); let promise; if (isResolved) { - promise = this.unResolveAll(projectPath, mergeRequestId, discussionId); + promise = this.unResolveAll(mergeRequestId, discussionId); } else { - promise = this.resolveAll(projectPath, mergeRequestId, discussionId); + promise = this.resolveAll(mergeRequestId, discussionId); } promise.then((response) => { @@ -62,11 +60,9 @@ }); } - resolveAll(projectPath, mergeRequestId, discussionId) { + resolveAll(mergeRequestId, discussionId) { const discussion = CommentsStore.state[discussionId]; - this.prepareRequest(projectPath); - discussion.loading = true; return this.discussionResource.save({ @@ -75,11 +71,9 @@ }, {}); } - unResolveAll(projectPath, mergeRequestId, discussionId) { + unResolveAll(mergeRequestId, discussionId) { const discussion = CommentsStore.state[discussionId]; - this.prepareRequest(projectPath); - discussion.loading = true; return this.discussionResource.delete({ @@ -89,5 +83,5 @@ } } - w.ResolveService = new ResolveServiceClass(); -})(window); + gl.DiffNotesResolveServiceClass = ResolveServiceClass; +})(); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index d108da29af7..3579843baed 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -455,7 +455,7 @@ require('vendor/task_list'); var mergeRequestId = $form.data('noteable-iid'); if (ResolveService != null) { - ResolveService.toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId); + ResolveService.toggleResolveForDiscussion(mergeRequestId, discussionId); } } diff --git a/app/views/discussions/_resolve_all.html.haml b/app/views/discussions/_resolve_all.html.haml index f0b61e0f7de..e30ee1b0e05 100644 --- a/app/views/discussions/_resolve_all.html.haml +++ b/app/views/discussions/_resolve_all.html.haml @@ -1,6 +1,5 @@ - if discussion.for_merge_request? - %resolve-discussion-btn{ ":project-path" => "'#{project_path(discussion.project)}'", - ":discussion-id" => "'#{discussion.id}'", + %resolve-discussion-btn{ ":discussion-id" => "'#{discussion.id}'", ":merge-request-id" => discussion.noteable.iid, ":can-resolve" => discussion.can_resolve?(current_user), "inline-template" => true } diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 83250443bea..dd615d3036c 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -3,10 +3,9 @@ - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('lib_vue') = page_specific_javascript_bundle_tag('diff_notes') -.merge-request{ 'data-url' => merge_request_path(@merge_request) } +.merge-request{ 'data-url' => merge_request_path(@merge_request), 'data-project-path' => project_path(@merge_request.project) } = render "projects/merge_requests/show/mr_title" .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } } diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 4b1da9c73e5..e58de9f0e18 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -30,8 +30,7 @@ - if note.resolvable? - can_resolve = can?(current_user, :resolve_note, note) - %resolve-btn{ "project-path" => "#{project_path(note.project)}", - "discussion-id" => "#{note.discussion_id}", + %resolve-btn{ "discussion-id" => "#{note.discussion_id}", ":note-id" => note.id, ":resolved" => note.resolved?, ":can-resolve" => can_resolve, From 88fb39c229ff6569b85417e6c63b87a77f97d667 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 9 Feb 2017 11:03:36 +0000 Subject: [PATCH 322/488] Uses shared vue resource interceptor --- app/assets/javascripts/diff_notes/services/resolve.js.es6 | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6 index d83a44ee205..090c454e9e4 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js.es6 +++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6 @@ -4,6 +4,7 @@ const Vue = window.Vue = require('vue'); window.Vue.use(require('vue-resource')); +require('../../vue_shared/vue_resource_interceptor'); (() => { window.gl = window.gl || {}; @@ -12,13 +13,6 @@ window.Vue.use(require('vue-resource')); constructor(root) { this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve`); this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`); - - Vue.http.interceptors.push((request, next) => { - if ($.rails) { - request.headers['X-CSRF-Token'] = $.rails.csrfToken(); - } - next(); - }); } resolve(noteId) { From 125b7232392c939f923cd2d08e714ab6ac699989 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 9 Feb 2017 16:02:57 +0000 Subject: [PATCH 323/488] Added missing filter word --- doc/user/project/issue_board.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index e7584bc1b83..3dcdc181d3d 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -100,8 +100,8 @@ add these issues to the selected list. By default the first list is selected, but this can be changed in the dropdown menu next to the **Add issues** button in the modal. -Within this modal you can also issues. This is done by using the filters at the -top of the modal. +Within this modal you can also filter issues. This is done by using the filters +at the top of the modal. You can filter by author, assignee, milestone and label. From edea801389e3996dfb2d312abc06d20534163764 Mon Sep 17 00:00:00 2001 From: Glenn Sayers Date: Thu, 9 Feb 2017 16:10:10 +0000 Subject: [PATCH 324/488] Allow copying a created branch name to the clipboard. --- app/views/projects/_last_push.html.haml | 1 + changelogs/unreleased/copy-branch-to-clipboard.yml | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 changelogs/unreleased/copy-branch-to-clipboard.yml diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml index 1c3bccccb5c..a08436715d2 100644 --- a/app/views/projects/_last_push.html.haml +++ b/app/views/projects/_last_push.html.haml @@ -10,6 +10,7 @@ - if @project && event.project != @project %span at %strong= link_to_project event.project + = clipboard_button(clipboard_text: event.ref_name, class: 'btn-clipboard btn-transparent', title: 'Copy branch to clipboard') #{time_ago_with_tooltip(event.created_at)} .pull-right diff --git a/changelogs/unreleased/copy-branch-to-clipboard.yml b/changelogs/unreleased/copy-branch-to-clipboard.yml new file mode 100644 index 00000000000..c12e324ed3c --- /dev/null +++ b/changelogs/unreleased/copy-branch-to-clipboard.yml @@ -0,0 +1,4 @@ +--- +title: Added the ability to copy a branch name to the clipboard +merge_request: 9103 +author: Glenn Sayers From 5d0c5663a0368545653be3992c364eea74830062 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 9 Feb 2017 16:36:32 +0000 Subject: [PATCH 325/488] Adds `.json` to the endpoint requested in order to avoid showing JSON --- .../javascripts/commit/pipelines/pipelines_service.js.es6 | 2 +- .../27932-merge-request-pipelines-displays-json.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/27932-merge-request-pipelines-displays-json.yml diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 index 483b414126a..e9cae893857 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 @@ -9,7 +9,7 @@ */ class PipelinesService { constructor(endpoint) { - this.pipelines = Vue.resource(endpoint); + this.pipelines = Vue.resource(`${endpoint}.json`); } /** diff --git a/changelogs/unreleased/27932-merge-request-pipelines-displays-json.yml b/changelogs/unreleased/27932-merge-request-pipelines-displays-json.yml new file mode 100644 index 00000000000..b7505e28401 --- /dev/null +++ b/changelogs/unreleased/27932-merge-request-pipelines-displays-json.yml @@ -0,0 +1,4 @@ +--- +title: Fix Merge request pipelines displays JSON +merge_request: +author: From fe04d920544c0234d22aa5a8c7c6793a99d3e267 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 9 Feb 2017 16:38:09 +0000 Subject: [PATCH 326/488] Update PROCESS.md Signed-off-by: Dmitriy Zaporozhets --- PROCESS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PROCESS.md b/PROCESS.md index f257c1d5358..fead93bd4cf 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -59,7 +59,7 @@ star, smile, etc.). Some good tips about code reviews can be found in our ## Feature Freeze -On the 7th of each month, RC1 of the upcoming release is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it. +After the 7th (Pacific Standard Time Zone) of each month, RC1 of the upcoming release is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it. Merge requests may still be merged into master during this period, but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch. By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things. From 7b4cd8c9f01ccaa3e07e026de9d1555f4b9edfa1 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 8 Feb 2017 12:33:12 -0600 Subject: [PATCH 327/488] exclude node_modules from imports-loader wrapper --- config/webpack.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/config/webpack.config.js b/config/webpack.config.js index 968c0076eaf..51a51e051a2 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -62,6 +62,7 @@ var config = { }, { test: /\.(js|es6)$/, + exclude: /node_modules/, loader: 'imports-loader', query: 'this=>window' }, From 31c10441fc658de0011e9bae3162c27d0ba937ef Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 8 Feb 2017 12:39:06 -0600 Subject: [PATCH 328/488] upgrade babel to v6 --- config/webpack.config.js | 5 +---- package.json | 7 ++++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index 51a51e051a2..2d1a16a18dd 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -54,10 +54,7 @@ var config = { exclude: /(node_modules|vendor\/assets)/, loader: 'babel-loader', query: { - // 'use strict' was broken in sprockets-es6 due to sprockets concatination method. - // many es5 strict errors which were never caught ended up in our es6 assets as a result. - // this hack is necessary until they can be fixed. - blacklist: ['useStrict'] + presets: ['es2015', 'stage-2'] } }, { diff --git a/package.json b/package.json index a25e09e4cf2..7f1c8dd6fff 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,10 @@ "webpack-prod": "NODE_ENV=production npm run webpack" }, "dependencies": { - "babel": "^5.8.38", - "babel-core": "^5.8.38", - "babel-loader": "^5.4.2", + "babel-core": "^6.22.1", + "babel-loader": "^6.2.10", + "babel-preset-es2015": "^6.22.0", + "babel-preset-stage-2": "^6.22.0", "bootstrap-sass": "3.3.6", "compression-webpack-plugin": "^0.3.2", "d3": "3.5.11", From a421258c498ce5746cef2bc90f86c738a6099366 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 8 Feb 2017 13:47:20 -0600 Subject: [PATCH 329/488] fix failing karma test --- .../filtered_search/filtered_search_dropdown.js.es6 | 2 +- spec/javascripts/filtered_search/dropdown_user_spec.js.es6 | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 859d6515531..e8c2df03a46 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -4,7 +4,7 @@ class FilteredSearchDropdown { constructor(droplab, dropdown, input, filter) { this.droplab = droplab; - this.hookId = input.getAttribute('data-id'); + this.hookId = input && input.getAttribute('data-id'); this.input = input; this.filter = filter; this.dropdown = dropdown; diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 index f4b0d60db34..fa9d03c8a9a 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 @@ -9,7 +9,7 @@ require('~/filtered_search/dropdown_user'); let dropdownUser; beforeEach(() => { - spyOn(gl.FilteredSearchDropdown.prototype, 'constructor').and.callFake(() => {}); + spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {}); @@ -39,7 +39,7 @@ require('~/filtered_search/dropdown_user'); describe('config droplabAjaxFilter\'s endpoint', () => { beforeEach(() => { - spyOn(gl.FilteredSearchDropdown.prototype, 'constructor').and.callFake(() => {}); + spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); }); From e92d5eb9d5c57e8ff9e7a1a5b908ad1ee0cd190e Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 8 Feb 2017 14:19:16 -0600 Subject: [PATCH 330/488] approve MIT license for wordwrap submodule --- config/dependency_decisions.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml index aabe859730a..54389eeb9ef 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -302,3 +302,9 @@ :why: https://github.com/dchest/tweetnacl-js/blob/master/LICENSE :versions: [] :when: 2017-01-14 20:10:57.812077000 Z +- - :approve + - wordwrap + - :who: Mike Greiling + :why: https://github.com/substack/node-wordwrap/blob/0.0.3/LICENSE + :versions: [] + :when: 2017-02-08 20:17:13.084968000 Z From 0a36693c92ce5ced61b68d2cb0d91694076d5d28 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 8 Feb 2017 16:52:58 -0600 Subject: [PATCH 331/488] add CHANGELOG.md entry for !9072 --- changelogs/unreleased/upgrade-babel-v6.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/upgrade-babel-v6.yml diff --git a/changelogs/unreleased/upgrade-babel-v6.yml b/changelogs/unreleased/upgrade-babel-v6.yml new file mode 100644 index 00000000000..55f9b3e407c --- /dev/null +++ b/changelogs/unreleased/upgrade-babel-v6.yml @@ -0,0 +1,4 @@ +--- +title: upgrade babel 5.8.x to babel 6.22.x +merge_request: 9072 +author: From 6dd90100bde153a62998f8db2204e4f59bca7940 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 8 Feb 2017 15:24:08 -0600 Subject: [PATCH 332/488] upgrade to webpack 2.2.x --- config/webpack.config.js | 27 ++++++++++++++------------- package.json | 8 +++----- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index 2d1a16a18dd..00f448c1fbb 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -48,24 +48,23 @@ var config = { devtool: 'inline-source-map', module: { - loaders: [ + rules: [ { test: /\.(js|es6)$/, exclude: /(node_modules|vendor\/assets)/, loader: 'babel-loader', - query: { - presets: ['es2015', 'stage-2'] + options: { + presets: [ + ["es2015", {"modules": false}], + 'stage-2' + ] } }, { test: /\.(js|es6)$/, exclude: /node_modules/, loader: 'imports-loader', - query: 'this=>window' - }, - { - test: /\.json$/, - loader: 'json-loader' + options: 'this=>window' } ] }, @@ -86,7 +85,7 @@ var config = { ], resolve: { - extensions: ['', '.js', '.es6', '.js.es6'], + extensions: ['.js', '.es6', '.js.es6'], alias: { '~': path.join(ROOT_PATH, 'app/assets/javascripts'), 'bootstrap/js': 'bootstrap-sass/assets/javascripts/bootstrap', @@ -102,14 +101,16 @@ if (IS_PRODUCTION) { config.devtool = 'source-map'; config.plugins.push( new webpack.NoErrorsPlugin(), + new webpack.LoaderOptionsPlugin({ + minimize: true, + debug: false + }), new webpack.optimize.UglifyJsPlugin({ - compress: { warnings: false } + sourceMap: true }), new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify('production') } - }), - new webpack.optimize.DedupePlugin(), - new webpack.optimize.OccurrenceOrderPlugin() + }) ); } diff --git a/package.json b/package.json index 7f1c8dd6fff..a012c49a72d 100644 --- a/package.json +++ b/package.json @@ -19,21 +19,19 @@ "compression-webpack-plugin": "^0.3.2", "d3": "3.5.11", "dropzone": "4.2.0", - "exports-loader": "^0.6.3", "imports-loader": "^0.6.5", "jquery": "2.2.1", "jquery-ui": "github:jquery/jquery-ui#1.11.4", "jquery-ujs": "1.2.1", - "json-loader": "^0.5.4", "mousetrap": "1.4.6", "pikaday": "^1.5.1", "select2": "3.5.2-browserify", - "stats-webpack-plugin": "^0.4.2", + "stats-webpack-plugin": "^0.4.3", "underscore": "1.8.3", "vue": "2.0.3", "vue-resource": "0.9.3", - "webpack": "^1.14.0", - "webpack-dev-server": "^1.16.2" + "webpack": "^2.2.1", + "webpack-dev-server": "^2.3.0" }, "devDependencies": { "eslint": "^3.10.1", From 0d60fa8dc52662923a645ac3ab278b172188b1a4 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 8 Feb 2017 15:59:41 -0600 Subject: [PATCH 333/488] update karma deps to work with webpack 2 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a012c49a72d..249c69f586a 100644 --- a/package.json +++ b/package.json @@ -42,10 +42,10 @@ "istanbul": "^0.4.5", "jasmine-core": "^2.5.2", "jasmine-jquery": "^2.1.1", - "karma": "^1.3.0", + "karma": "^1.4.1", "karma-jasmine": "^1.1.0", "karma-phantomjs-launcher": "^1.0.2", "karma-sourcemap-loader": "^0.3.7", - "karma-webpack": "^1.8.0" + "karma-webpack": "^2.0.2" } } From 1ac21cc5f00742e4d12ea3294f6a6b8f59c0cfc8 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 8 Feb 2017 16:36:42 -0600 Subject: [PATCH 334/488] approve new dependencies --- config/dependency_decisions.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml index 54389eeb9ef..7336d7c842a 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -308,3 +308,15 @@ :why: https://github.com/substack/node-wordwrap/blob/0.0.3/LICENSE :versions: [] :when: 2017-02-08 20:17:13.084968000 Z +- - :approve + - spdx-expression-parse + - :who: Mike Greiling + :why: https://github.com/kemitchell/spdx-expression-parse.js/blob/v1.0.4/LICENSE + :versions: [] + :when: 2017-02-08 22:33:01.806977000 Z +- - :approve + - spdx-license-ids + - :who: Mike Greiling + :why: https://github.com/shinnn/spdx-license-ids/blob/v1.2.2/LICENSE + :versions: [] + :when: 2017-02-08 22:35:00.225232000 Z From 63884e5f098fb1823bbd75bc6b13cb13652aa631 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 8 Feb 2017 16:55:53 -0600 Subject: [PATCH 335/488] add CHANGELOG.md entry for !9078 --- changelogs/unreleased/upgrade-webpack-v2-2.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/upgrade-webpack-v2-2.yml diff --git a/changelogs/unreleased/upgrade-webpack-v2-2.yml b/changelogs/unreleased/upgrade-webpack-v2-2.yml new file mode 100644 index 00000000000..6a49859d68c --- /dev/null +++ b/changelogs/unreleased/upgrade-webpack-v2-2.yml @@ -0,0 +1,4 @@ +--- +title: upgrade to webpack v2.2 +merge_request: 9078 +author: From f57989c2ed15c5d84f198ca334686f5bf7207f8e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 8 Feb 2017 16:22:36 -0500 Subject: [PATCH 336/488] Add a spec for our custom GemFetcher cop --- .rubocop.yml | 22 +++++++------ rubocop/cop/gem_fetcher.rb | 23 +++++++++----- rubocop/rubocop.rb | 4 +-- spec/rubocop/cop/gem_fetcher_spec.rb | 46 ++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 spec/rubocop/cop/gem_fetcher_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index cfff42e5c99..88345373a5b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -31,8 +31,7 @@ AllCops: - 'lib/gitlab/seeder.rb' - 'generator_templates/**/*' - -##################### Style ################################## +# Style ####################################################################### # Check indentation of private/protected visibility modifiers. Style/AccessModifierIndentation: @@ -471,7 +470,7 @@ Style/WhileUntilModifier: Style/WordArray: Enabled: false -#################### Metrics ################################ +# Metrics ##################################################################### # A calculated magnitude based on number of assignments, # branches, and conditions. @@ -516,8 +515,7 @@ Metrics/PerceivedComplexity: Enabled: true Max: 18 - -#################### Lint ################################ +# Lint ######################################################################## # Checks for useless access modifiers. Lint/UselessAccessModifier: @@ -679,8 +677,7 @@ Lint/UselessSetterCall: Lint/Void: Enabled: true - -##################### Performance ############################ +# Performance ################################################################# # Use `casecmp` rather than `downcase ==`. Performance/Casecmp: @@ -718,8 +715,7 @@ Performance/StringReplacement: Performance/TimesMap: Enabled: true - -##################### Rails ################################## +# Rails ####################################################################### # Enables Rails cops. Rails: @@ -767,7 +763,7 @@ Rails/ReadWriteAttribute: Rails/ScopeArgs: Enabled: true -##################### RSpec ################################## +# RSpec ####################################################################### # Check that instances are not being stubbed globally. RSpec/AnyInstance: @@ -828,3 +824,9 @@ RSpec/NotToNot: # Prefer using verifying doubles over normal doubles. RSpec/VerifiedDoubles: Enabled: false + +# Custom ###################################################################### + +# Disallow the `git` and `github` arguments in the Gemfile. +GemFetcher: + Enabled: true diff --git a/rubocop/cop/gem_fetcher.rb b/rubocop/cop/gem_fetcher.rb index 4a63c760744..c199f6acab2 100644 --- a/rubocop/cop/gem_fetcher.rb +++ b/rubocop/cop/gem_fetcher.rb @@ -1,17 +1,15 @@ module RuboCop module Cop - # Cop that checks for all gems specified in the Gemfile, and will - # alert if any gem is to be fetched not from the RubyGems index. - # This enforcement is done so as to minimize external build - # dependencies and build times. + # This cop prevents usage of the `git` and `github` arguments to `gem` in a + # `Gemfile` in order to avoid additional points of failure beyond + # rubygems.org. class GemFetcher < RuboCop::Cop::Cop MSG = 'Do not use gems from git repositories, only use gems from RubyGems.' GIT_KEYS = [:git, :github] def on_send(node) - file_path = node.location.expression.source_buffer.name - return unless file_path.end_with?("Gemfile") + return unless gemfile?(node) func_name = node.children[1] return unless func_name == :gem @@ -19,10 +17,21 @@ module RuboCop node.children.last.each_node(:pair) do |pair| key_name = pair.children[0].children[0].to_sym if GIT_KEYS.include?(key_name) - add_offense(node, :selector) + add_offense(node, pair.source_range, MSG) end end end + + private + + def gemfile?(node) + node + .location + .expression + .source_buffer + .name + .end_with?("Gemfile") + end end end end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index 3e292a4527c..aa35fb1701c 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -1,4 +1,4 @@ -require_relative 'cop/migration/add_index' +require_relative 'cop/gem_fetcher' require_relative 'cop/migration/add_column' require_relative 'cop/migration/add_column_with_default' -require_relative 'cop/gem_fetcher' +require_relative 'cop/migration/add_index' diff --git a/spec/rubocop/cop/gem_fetcher_spec.rb b/spec/rubocop/cop/gem_fetcher_spec.rb new file mode 100644 index 00000000000..c07f6a831dc --- /dev/null +++ b/spec/rubocop/cop/gem_fetcher_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../rubocop/cop/gem_fetcher' + +describe RuboCop::Cop::GemFetcher do + include CopHelper + + subject(:cop) { described_class.new } + + context 'in Gemfile' do + before do + allow(cop).to receive(:gemfile?).and_return(true) + end + + it 'registers an offense when a gem uses `git`' do + inspect_source(cop, 'gem "foo", git: "https://gitlab.com/foo/bar.git"') + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + expect(cop.highlights).to eq(['git: "https://gitlab.com/foo/bar.git"']) + end + end + + it 'registers an offense when a gem uses `github`' do + inspect_source(cop, 'gem "foo", github: "foo/bar.git"') + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + expect(cop.highlights).to eq(['github: "foo/bar.git"']) + end + end + end + + context 'outside of Gemfile' do + it 'registers no offense' do + inspect_source(cop, 'gem "foo", git: "https://gitlab.com/foo/bar.git"') + + expect(cop.offenses.size).to eq(0) + end + end +end From bc261b082063103cd16eec30c3d7ab9b487a6ece Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 9 Feb 2017 17:23:36 +0000 Subject: [PATCH 337/488] Adds a null check to build notifications Closes #27948 --- app/assets/javascripts/merge_request_widget.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index e5d2d706fc7..69aed77c83d 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -154,7 +154,7 @@ require('./smart_interval'); return $.getJSON(this.opts.ci_status_url, (function(_this) { return function(data) { var message, status, title; - if (data.status === '') { + if (!data.status) { return; } if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); From 877f899f1e9311fcfd9917a39f74c7364e1ca45e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 9 Feb 2017 10:02:54 -0600 Subject: [PATCH 338/488] Fix contribution activity alignment --- app/assets/stylesheets/framework/calendar.scss | 2 ++ ...943-contribution-list-on-profile-page-is-aligned-right.yml | 4 ++++ 2 files changed, 6 insertions(+) create mode 100644 changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index d485e75a434..fb8ea18d122 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -9,6 +9,8 @@ } .user-calendar-activities { + direction: ltr; + .str-truncated { max-width: 70%; } diff --git a/changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml b/changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml new file mode 100644 index 00000000000..fcbd48b0357 --- /dev/null +++ b/changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml @@ -0,0 +1,4 @@ +--- +title: Fix contribution activity alignment +merge_request: +author: From 6fab6d94cef853ed0d081dcea0fbfe390047b1c8 Mon Sep 17 00:00:00 2001 From: Joost Rijneveld Date: Fri, 3 Feb 2017 15:49:27 +0100 Subject: [PATCH 339/488] Optionally make users created via the API set their password --- .../1051-api-create-users-without-password.yml | 4 ++++ doc/api/users.md | 5 +++-- lib/api/users.rb | 16 ++++++++++++++-- spec/requests/api/users_spec.rb | 12 ++++++++++++ 4 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/1051-api-create-users-without-password.yml diff --git a/changelogs/unreleased/1051-api-create-users-without-password.yml b/changelogs/unreleased/1051-api-create-users-without-password.yml new file mode 100644 index 00000000000..24b5a73b45c --- /dev/null +++ b/changelogs/unreleased/1051-api-create-users-without-password.yml @@ -0,0 +1,4 @@ +--- +title: Optionally make users created via the API set their password +merge_request: 8957 +author: Joost Rijneveld diff --git a/doc/api/users.md b/doc/api/users.md index fea9bdf9639..ed3469521fc 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -216,7 +216,7 @@ Parameters: ## User creation -Creates a new user. Note only administrators can create new users. +Creates a new user. Note only administrators can create new users. Either `password` or `reset_password` should be specified (`reset_password` takes priority). ``` POST /users @@ -225,7 +225,8 @@ POST /users Parameters: - `email` (required) - Email -- `password` (required) - Password +- `password` (optional) - Password +- `reset_password` (optional) - Send user password reset link - true or false(default) - `username` (required) - Username - `name` (required) - Name - `skype` (optional) - Skype ID diff --git a/lib/api/users.rb b/lib/api/users.rb index 0ed468626b7..500697af633 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -82,7 +82,9 @@ module API end params do requires :email, type: String, desc: 'The email of the user' - requires :password, type: String, desc: 'The password of the new user' + optional :password, type: String, desc: 'The password of the new user' + optional :reset_password, type: Boolean, desc: 'Flag indicating the user will be sent a password reset token' + at_least_one_of :password, :reset_password requires :name, type: String, desc: 'The name of the user' requires :username, type: String, desc: 'The username of the user' use :optional_attributes @@ -94,8 +96,18 @@ module API user_params = declared_params(include_missing: false) identity_attrs = user_params.slice(:provider, :extern_uid) confirm = user_params.delete(:confirm) + user = User.new(user_params.except(:extern_uid, :provider, :reset_password)) + + if user_params.delete(:reset_password) + user.attributes = { + force_random_password: true, + password_expires_at: nil, + created_by_id: current_user.id + } + user.generate_password + user.generate_reset_token + end - user = User.new(user_params.except(:extern_uid, :provider)) user.skip_confirmation! unless confirm if identity_attrs.any? diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 8692f9da976..5958012672e 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -190,6 +190,18 @@ describe API::Users, api: true do expect(new_user.external).to be_truthy end + it "creates user with reset password" do + post api('/users', admin), attributes_for(:user, reset_password: true).except(:password) + + expect(response).to have_http_status(201) + + user_id = json_response['id'] + new_user = User.find(user_id) + + expect(new_user).not_to eq(nil) + expect(new_user.recently_sent_password_reset?).to eq(true) + end + it "does not create user with invalid email" do post api('/users', admin), email: 'invalid email', From 0b14b654b6e5d936f7241dcc0c249e0d4cc42728 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Mon, 23 Jan 2017 18:40:25 -0200 Subject: [PATCH 340/488] Gather issuable metadata to avoid n+ queries on index view --- .../concerns/issuable_collections.rb | 22 +++++++ app/controllers/concerns/issues_action.rb | 3 + .../concerns/merge_requests_action.rb | 3 + app/controllers/projects/issues_controller.rb | 7 ++- .../projects/merge_requests_controller.rb | 7 ++- app/models/award_emoji.rb | 8 +++ app/models/concerns/issuable.rb | 5 ++ app/models/note.rb | 6 ++ app/views/projects/issues/_issue.html.haml | 17 +----- .../merge_requests/_merge_request.html.haml | 17 +----- .../shared/_issuable_meta_data.html.haml | 19 +++++++ changelogs/unreleased/issue_25900.yml | 4 ++ spec/controllers/dashboard_controller_spec.rb | 19 +++++++ .../projects/issues_controller_spec.rb | 2 + .../merge_requests_controller_spec.rb | 4 ++ spec/features/issuables/issuable_list_spec.rb | 57 +++++++++++++++++++ ...issuables_list_metadata_shared_examples.rb | 35 ++++++++++++ 17 files changed, 199 insertions(+), 36 deletions(-) create mode 100644 app/views/shared/_issuable_meta_data.html.haml create mode 100644 changelogs/unreleased/issue_25900.yml create mode 100644 spec/controllers/dashboard_controller_spec.rb create mode 100644 spec/features/issuables/issuable_list_spec.rb create mode 100644 spec/support/issuables_list_metadata_shared_examples.rb diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 6247934f81e..a6e158ebae6 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -9,6 +9,28 @@ module IssuableCollections private + def issuable_meta_data(issuable_collection) + # map has to be used here since using pluck or select will + # throw an error when ordering issuables by priority which inserts + # a new order into the collection. + # We cannot use reorder to not mess up the paginated collection. + issuable_ids = issuable_collection.map(&:id) + issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type) + issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type) + + issuable_ids.each_with_object({}) do |id, issuable_meta| + downvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? } + upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? } + notes = issuable_note_count.find { |notes| notes.noteable_id == id } + + issuable_meta[id] = Issuable::IssuableMeta.new( + upvotes.try(:count).to_i, + downvotes.try(:count).to_i, + notes.try(:count).to_i + ) + end + end + def issues_collection issues_finder.execute.preload(:project, :author, :assignee, :labels, :milestone, project: :namespace) end diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index b46adcceb60..fb5edb34370 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -9,6 +9,9 @@ module IssuesAction .non_archived .page(params[:page]) + @collection_type = "Issue" + @issuable_meta_data = issuable_meta_data(@issues) + respond_to do |format| format.html format.atom { render layout: false } diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb index fdb05bb3228..6229759dcf1 100644 --- a/app/controllers/concerns/merge_requests_action.rb +++ b/app/controllers/concerns/merge_requests_action.rb @@ -7,6 +7,9 @@ module MergeRequestsAction @merge_requests = merge_requests_collection .page(params[:page]) + + @collection_type = "MergeRequest" + @issuable_meta_data = issuable_meta_data(@merge_requests) end private diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index c75b8987a4b..744a4af1c51 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -23,8 +23,11 @@ class Projects::IssuesController < Projects::ApplicationController respond_to :html def index - @issues = issues_collection - @issues = @issues.page(params[:page]) + @collection_type = "Issue" + @issues = issues_collection + @issues = @issues.page(params[:page]) + @issuable_meta_data = issuable_meta_data(@issues) + if @issues.out_of_range? && @issues.total_pages != 0 return redirect_to url_for(params.merge(page: @issues.total_pages)) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index fbad66c5c40..c3e1760f168 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -36,8 +36,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :conflict_for_path, :resolve_conflicts] def index - @merge_requests = merge_requests_collection - @merge_requests = @merge_requests.page(params[:page]) + @collection_type = "MergeRequest" + @merge_requests = merge_requests_collection + @merge_requests = @merge_requests.page(params[:page]) + @issuable_meta_data = issuable_meta_data(@merge_requests) + if @merge_requests.out_of_range? && @merge_requests.total_pages != 0 return redirect_to url_for(params.merge(page: @merge_requests.total_pages)) end diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index 46b17479d6d..6937ad3bdd9 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -16,6 +16,14 @@ class AwardEmoji < ActiveRecord::Base scope :downvotes, -> { where(name: DOWNVOTE_NAME) } scope :upvotes, -> { where(name: UPVOTE_NAME) } + class << self + def votes_for_collection(ids, type) + select('name', 'awardable_id', 'COUNT(*) as count'). + where('name IN (?) AND awardable_type = ? AND awardable_id IN (?)', [DOWNVOTE_NAME, UPVOTE_NAME], type, ids). + group('name', 'awardable_id') + end + end + def downvote? self.name == DOWNVOTE_NAME end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 3517969eabc..bfb54e878fc 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -15,6 +15,11 @@ module Issuable include Taskable include TimeTrackable + # This object is used to gather issuable meta data for displaying + # upvotes, downvotes and notes count for issues and merge requests + # lists avoiding n+1 queries and improving performance. + IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count) + included do cache_markdown_field :title, pipeline: :single_line cache_markdown_field :description diff --git a/app/models/note.rb b/app/models/note.rb index bf090a0438c..029fe667a45 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -108,6 +108,12 @@ class Note < ActiveRecord::Base Discussion.for_diff_notes(active_notes). map { |d| [d.line_code, d] }.to_h end + + def count_for_collection(ids, type) + user.select('noteable_id', 'COUNT(*) as count'). + group(:noteable_id). + where(noteable_type: type, noteable_id: ids) + end end def cross_reference? diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 5c9839cb330..0e3902c066a 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -17,22 +17,7 @@ %li = link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name") - - upvotes, downvotes = issue.upvotes, issue.downvotes - - if upvotes > 0 - %li - = icon('thumbs-up') - = upvotes - - - if downvotes > 0 - %li - = icon('thumbs-down') - = downvotes - - - note_count = issue.notes.user.count - %li - = link_to issue_path(issue, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do - = icon('comments') - = note_count + = render 'shared/issuable_meta_data', issuable: issue .issue-info #{issuable_reference(issue)} · diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index a5fbe9d6128..11b7aaec704 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -29,22 +29,7 @@ %li = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name") - - upvotes, downvotes = merge_request.upvotes, merge_request.downvotes - - if upvotes > 0 - %li - = icon('thumbs-up') - = upvotes - - - if downvotes > 0 - %li - = icon('thumbs-down') - = downvotes - - - note_count = merge_request.related_notes.user.count - %li - = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do - = icon('comments') - = note_count + = render 'shared/issuable_meta_data', issuable: merge_request .merge-request-info #{issuable_reference(merge_request)} · diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml new file mode 100644 index 00000000000..1264e524d86 --- /dev/null +++ b/app/views/shared/_issuable_meta_data.html.haml @@ -0,0 +1,19 @@ +- note_count = @issuable_meta_data[issuable.id].notes_count +- issue_votes = @issuable_meta_data[issuable.id] +- upvotes, downvotes = issue_votes.upvotes, issue_votes.downvotes +- issuable_url = @collection_type == "Issue" ? issue_path(issuable, anchor: 'notes') : merge_request_path(issuable, anchor: 'notes') + +- if upvotes > 0 + %li + = icon('thumbs-up') + = upvotes + +- if downvotes > 0 + %li + = icon('thumbs-down') + = downvotes + +%li + = link_to issuable_url, class: ('no-comments' if note_count.zero?) do + = icon('comments') + = note_count diff --git a/changelogs/unreleased/issue_25900.yml b/changelogs/unreleased/issue_25900.yml new file mode 100644 index 00000000000..b4b72b8a20c --- /dev/null +++ b/changelogs/unreleased/issue_25900.yml @@ -0,0 +1,4 @@ +--- +title: Gather issuable metadata to avoid n+1 queries on index view +merge_request: +author: diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb new file mode 100644 index 00000000000..566d8515198 --- /dev/null +++ b/spec/controllers/dashboard_controller_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe DashboardController do + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + project.team << [user, :master] + sign_in(user) + end + + describe 'GET issues' do + it_behaves_like 'issuables list meta-data', :issue, :issues + end + + describe 'GET merge requests' do + it_behaves_like 'issuables list meta-data', :merge_request, :merge_requests + end +end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 4b89381eb96..e576bf9ef79 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -24,6 +24,8 @@ describe Projects::IssuesController do project.team << [user, :developer] end + it_behaves_like "issuables list meta-data", :issue + it "returns index" do get :index, namespace_id: project.namespace.path, project_id: project.path diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 63780802cfa..bfd134e406e 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -147,6 +147,8 @@ describe Projects::MergeRequestsController do end describe 'GET index' do + let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } + def get_merge_requests(page = nil) get :index, namespace_id: project.namespace.to_param, @@ -154,6 +156,8 @@ describe Projects::MergeRequestsController do state: 'opened', page: page.to_param end + it_behaves_like "issuables list meta-data", :merge_request + context 'when page param' do let(:last_page) { project.merge_requests.page().total_pages } let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb new file mode 100644 index 00000000000..e31bc40adc3 --- /dev/null +++ b/spec/features/issuables/issuable_list_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +describe 'issuable list', feature: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + issuable_types = [:issue, :merge_request] + + before do + project.add_user(user, :developer) + login_as(user) + issuable_types.each { |type| create_issuables(type) } + end + + issuable_types.each do |issuable_type| + it "avoids N+1 database queries for #{issuable_type.to_s.humanize.pluralize}" do + control_count = ActiveRecord::QueryRecorder.new { visit_issuable_list(issuable_type) }.count + + create_issuables(issuable_type) + + expect { visit_issuable_list(issuable_type) }.not_to exceed_query_limit(control_count) + end + + it "counts upvotes, downvotes and notes count for each #{issuable_type.to_s.humanize}" do + visit_issuable_list(issuable_type) + + expect(first('.fa-thumbs-up').find(:xpath, '..')).to have_content(1) + expect(first('.fa-thumbs-down').find(:xpath, '..')).to have_content(1) + expect(first('.fa-comments').find(:xpath, '..')).to have_content(2) + end + end + + def visit_issuable_list(issuable_type) + if issuable_type == :issue + visit namespace_project_issues_path(project.namespace, project) + else + visit namespace_project_merge_requests_path(project.namespace, project) + end + end + + def create_issuables(issuable_type) + 3.times do + if issuable_type == :issue + issuable = create(:issue, project: project, author: user) + else + issuable = create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name) + end + + 2.times do + create(:note_on_issue, noteable: issuable, project: project, note: 'Test note') + end + + create(:award_emoji, :downvote, awardable: issuable) + create(:award_emoji, :upvote, awardable: issuable) + end + end +end diff --git a/spec/support/issuables_list_metadata_shared_examples.rb b/spec/support/issuables_list_metadata_shared_examples.rb new file mode 100644 index 00000000000..dac94dfc31e --- /dev/null +++ b/spec/support/issuables_list_metadata_shared_examples.rb @@ -0,0 +1,35 @@ +shared_examples 'issuables list meta-data' do |issuable_type, action = nil| + before do + @issuable_ids = [] + + 2.times do + if issuable_type == :issue + issuable = create(issuable_type, project: project) + else + issuable = create(issuable_type, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name) + end + + @issuable_ids << issuable.id + + issuable.id.times { create(:note, noteable: issuable, project: issuable.project) } + (issuable.id + 1).times { create(:award_emoji, :downvote, awardable: issuable) } + (issuable.id + 2).times { create(:award_emoji, :upvote, awardable: issuable) } + end + end + + it "creates indexed meta-data object for issuable notes and votes count" do + if action + get action + else + get :index, namespace_id: project.namespace.path, project_id: project.path + end + + meta_data = assigns(:issuable_meta_data) + + @issuable_ids.each do |id| + expect(meta_data[id].notes_count).to eq(id) + expect(meta_data[id].downvotes).to eq(id + 1) + expect(meta_data[id].upvotes).to eq(id + 2) + end + end +end From 3559c0094cff2e57909db5a477c3004ec7c32386 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 9 Feb 2017 15:20:34 -0600 Subject: [PATCH 341/488] Make min width smaller for user settings --- app/assets/stylesheets/framework/header.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 2a01bc4d44d..731ce57c245 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -272,7 +272,7 @@ header { .header-user { .dropdown-menu-nav { - width: 140px; + min-width: 140px; margin-top: -5px; } } From 0d9cce410dd146e512dc84fdb39bff13929a1362 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 9 Feb 2017 20:21:31 -0500 Subject: [PATCH 342/488] Remove a transient failure from spec/requests/api/groups_spec.rb --- spec/requests/api/groups_spec.rb | 8 +++++--- spec/support/matchers/satisfy_matchers.rb | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 spec/support/matchers/satisfy_matchers.rb diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 15592f1f702..f78bde6f53a 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -35,7 +35,8 @@ describe API::Groups, api: true do expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) - expect(json_response.first['name']).to eq(group1.name) + expect(json_response) + .to satisfy_one { |group| group['name'] == group1.name } end it "does not include statistics" do @@ -70,7 +71,7 @@ describe API::Groups, api: true do repository_size: 123, lfs_objects_size: 234, build_artifacts_size: 345, - } + }.stringify_keys project1.statistics.update!(attributes) @@ -78,7 +79,8 @@ describe API::Groups, api: true do expect(response).to have_http_status(200) expect(json_response).to be_an Array - expect(json_response.first['statistics']).to eq attributes.stringify_keys + expect(json_response) + .to satisfy_one { |group| group['statistics'] == attributes } end end diff --git a/spec/support/matchers/satisfy_matchers.rb b/spec/support/matchers/satisfy_matchers.rb new file mode 100644 index 00000000000..585915bac93 --- /dev/null +++ b/spec/support/matchers/satisfy_matchers.rb @@ -0,0 +1,19 @@ +# These matchers are a syntactic hack to provide more readable expectations for +# an Enumerable object. +# +# They take advantage of the `all?`, `none?`, and `one?` methods, and the fact +# that RSpec provides a `be_something` matcher for all predicates. +# +# Example: +# +# # Ensure exactly one object in an Array satisfies a condition +# expect(users.one? { |u| u.admin? }).to eq true +# +# # The same thing, but using the `be_one` matcher +# expect(users).to be_one { |u| u.admin? } +# +# # The same thing again, but using `satisfy_one` for improved readability +# expect(users).to satisfy_one { |u| u.admin? } +RSpec::Matchers.alias_matcher :satisfy_all, :be_all +RSpec::Matchers.alias_matcher :satisfy_none, :be_none +RSpec::Matchers.alias_matcher :satisfy_one, :be_one From b9bd836f9aa5649b638ee1d829400ee97a6a01e3 Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Fri, 10 Feb 2017 11:52:03 +0600 Subject: [PATCH 343/488] fixes frontend doc broken link --- doc/development/frontend.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/frontend.md b/doc/development/frontend.md index 75fdf3d8e63..1f0dfb5e9d0 100644 --- a/doc/development/frontend.md +++ b/doc/development/frontend.md @@ -50,7 +50,7 @@ Let's look into each of them: This is the index file of your new feature. This is where the root Vue instance of the new feature should be. -Don't forget to follow [these steps.][page-specific-javascript] +Don't forget to follow [these steps.][page_specific_javascript] **A folder for Components** From be88e942a85a51ad267502e577ee578f58f3f226 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 10 Feb 2017 01:07:11 -0600 Subject: [PATCH 344/488] Show Pipeline(not Job) in MR desktop notification Fix https://gitlab.com/gitlab-org/gitlab-ce/issues/27955 --- app/views/projects/merge_requests/widget/_show.html.haml | 4 ++-- .../27955-mr-notification-use-pipeline-language.yml | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 5de59473840..4c063747857 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -16,13 +16,13 @@ gitlab_icon: "#{asset_path 'gitlab_logo.png'}", ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}", ci_message: { - normal: "Job {{status}} for \"{{title}}\"", + normal: "Pipeline {{status}} for \"{{title}}\"", preparing: "{{status}} job for \"{{title}}\"" }, ci_enable: #{@project.ci_service ? "true" : "false"}, ci_title: { preparing: "{{status}} job", - normal: "Job {{status}}" + normal: "Pipeline {{status}}" }, ci_sha: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.short_sha : ''}", ci_pipeline: #{@merge_request.head_pipeline.try(:id).to_json}, diff --git a/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml b/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml new file mode 100644 index 00000000000..d9f78db4bec --- /dev/null +++ b/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml @@ -0,0 +1,4 @@ +--- +title: Show Pipeline(not Job) in MR desktop notification +merge_request: +author: From 7c778df523c4b16b05490ef0a2d4b9f1f78deadf Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Fri, 10 Feb 2017 13:12:01 +0600 Subject: [PATCH 345/488] adds changelog --- changelogs/unreleased/27783-fix-fe-doc-broken-link.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/27783-fix-fe-doc-broken-link.yml diff --git a/changelogs/unreleased/27783-fix-fe-doc-broken-link.yml b/changelogs/unreleased/27783-fix-fe-doc-broken-link.yml new file mode 100644 index 00000000000..429110e9178 --- /dev/null +++ b/changelogs/unreleased/27783-fix-fe-doc-broken-link.yml @@ -0,0 +1,4 @@ +--- +title: Fixes FE Doc broken link +merge_request: 9120 +author: From 9eb81331457a480251218dd837867fd6e2a6e3f6 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 10 Feb 2017 16:33:10 +0800 Subject: [PATCH 346/488] Make sure our current .gitlab-ci.yml is valid This could prevent errors described in: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8970 Everything we're using right now, should be valid of course. --- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 008c15c4de3..68ad429608d 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -4,6 +4,16 @@ module Ci describe GitlabCiYamlProcessor, lib: true do let(:path) { 'path' } + describe 'our current .gitlab-ci.yml' do + let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") } + + it 'is valid' do + error_message = described_class.validation_message(config) + + expect(error_message).to be_nil + end + end + describe '#build_attributes' do describe 'coverage entry' do subject { described_class.new(config, path).build_attributes(:rspec) } From 3a27ec45e1dd4062ffaf10c38b4405fb5ff9c128 Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Fri, 10 Feb 2017 14:41:25 +0600 Subject: [PATCH 347/488] moved hyperlink reference section at the end of the content --- doc/development/frontend.md | 84 +++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/doc/development/frontend.md b/doc/development/frontend.md index 1f0dfb5e9d0..474769fb553 100644 --- a/doc/development/frontend.md +++ b/doc/development/frontend.md @@ -323,47 +323,6 @@ gl.MyThing = MyThing; For our currently-supported browsers, see our [requirements][requirements]. -[rails]: http://rubyonrails.org/ -[haml]: http://haml.info/ -[hamlit]: https://github.com/k0kubun/hamlit -[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations -[scss]: http://sass-lang.com/ -[es6]: https://babeljs.io/ -[sprockets]: https://github.com/rails/sprockets -[jquery]: https://jquery.com/ -[vue]: http://vuejs.org/ -[vue-docs]: http://vuejs.org/guide/index.html -[web-page-test]: http://www.webpagetest.org/ -[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/ -[google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en -[browser-diet]: https://browserdiet.com/ -[d3]: https://d3js.org/ -[chartjs]: http://www.chartjs.org/ -[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8 -[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools -[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules -[observatory-cli]: https://github.com/mozilla/http-observatory-cli -[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html -[secure_headers]: https://github.com/twitter/secureheaders -[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP -[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/ -[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/ -[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/ -[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/ -[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/ -[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity -[github-eng-sri]: http://githubengineering.com/subresource-integrity/ -[sprockets-sri]: https://github.com/rails/sprockets-rails#sri-support -[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting -[scss-style-guide]: scss_styleguide.md -[requirements]: ../install/requirements.md#supported-web-browsers -[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards -[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments -[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript -[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components -[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch -[vue-resource-repo]: https://github.com/pagekit/vue-resource -[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6 ## Gotchas @@ -438,3 +397,46 @@ Scenario: Developer can approve merge request Then I should see approved merge request "Bug NS-04" ``` + + +[rails]: http://rubyonrails.org/ +[haml]: http://haml.info/ +[hamlit]: https://github.com/k0kubun/hamlit +[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations +[scss]: http://sass-lang.com/ +[es6]: https://babeljs.io/ +[sprockets]: https://github.com/rails/sprockets +[jquery]: https://jquery.com/ +[vue]: http://vuejs.org/ +[vue-docs]: http://vuejs.org/guide/index.html +[web-page-test]: http://www.webpagetest.org/ +[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/ +[google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en +[browser-diet]: https://browserdiet.com/ +[d3]: https://d3js.org/ +[chartjs]: http://www.chartjs.org/ +[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8 +[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools +[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules +[observatory-cli]: https://github.com/mozilla/http-observatory-cli +[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html +[secure_headers]: https://github.com/twitter/secureheaders +[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP +[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/ +[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/ +[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/ +[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/ +[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/ +[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity +[github-eng-sri]: http://githubengineering.com/subresource-integrity/ +[sprockets-sri]: https://github.com/rails/sprockets-rails#sri-support +[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting +[scss-style-guide]: scss_styleguide.md +[requirements]: ../install/requirements.md#supported-web-browsers +[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards +[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments +[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript +[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components +[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch +[vue-resource-repo]: https://github.com/pagekit/vue-resource +[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6 From ee698e9595ac72fb4ec91f0d443bde62793dbf2b Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 10 Feb 2017 03:41:39 -0500 Subject: [PATCH 348/488] Restore exposure of legend property for events --- app/serializers/analytics_stage_entity.rb | 1 + app/views/projects/cycle_analytics/show.html.haml | 2 +- lib/gitlab/cycle_analytics/code_stage.rb | 4 ++++ lib/gitlab/cycle_analytics/issue_stage.rb | 4 ++++ lib/gitlab/cycle_analytics/plan_stage.rb | 4 ++++ lib/gitlab/cycle_analytics/production_stage.rb | 4 ++++ lib/gitlab/cycle_analytics/review_stage.rb | 4 ++++ lib/gitlab/cycle_analytics/staging_stage.rb | 4 ++++ lib/gitlab/cycle_analytics/test_stage.rb | 4 ++++ 9 files changed, 30 insertions(+), 1 deletion(-) diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb index a559d0850c4..69bf693de8d 100644 --- a/app/serializers/analytics_stage_entity.rb +++ b/app/serializers/analytics_stage_entity.rb @@ -2,6 +2,7 @@ class AnalyticsStageEntity < Grape::Entity include EntityDateHelper expose :title + expose :legend expose :description expose :median, as: :value do |stage| diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 5405ff16bea..ad904a8708e 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -44,7 +44,7 @@ Last 90 days .stage-panel-container .panel.panel-default.stage-panel - .panel-heading + .panel-heading %nav.col-headers %ul %li.stage-header diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb index d1bc2055ba8..1e52b6614a1 100644 --- a/lib/gitlab/cycle_analytics/code_stage.rb +++ b/lib/gitlab/cycle_analytics/code_stage.rb @@ -13,6 +13,10 @@ module Gitlab :code end + def legend + "Related Merge Requests" + end + def description "Time until first merge request" end diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb index d2068fbc38f..213994988a5 100644 --- a/lib/gitlab/cycle_analytics/issue_stage.rb +++ b/lib/gitlab/cycle_analytics/issue_stage.rb @@ -14,6 +14,10 @@ module Gitlab :issue end + def legend + "Related Issues" + end + def description "Time before an issue gets scheduled" end diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb index 3b4dfc6a30e..45d51d30ccc 100644 --- a/lib/gitlab/cycle_analytics/plan_stage.rb +++ b/lib/gitlab/cycle_analytics/plan_stage.rb @@ -14,6 +14,10 @@ module Gitlab :plan end + def legend + "Related Commits" + end + def description "Time before an issue starts implementation" end diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb index 2a6bcc80116..9f387a02945 100644 --- a/lib/gitlab/cycle_analytics/production_stage.rb +++ b/lib/gitlab/cycle_analytics/production_stage.rb @@ -15,6 +15,10 @@ module Gitlab :production end + def legend + "Related Issues" + end + def description "From issue creation until deploy to production" end diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb index fbaa3010d81..4744be834de 100644 --- a/lib/gitlab/cycle_analytics/review_stage.rb +++ b/lib/gitlab/cycle_analytics/review_stage.rb @@ -13,6 +13,10 @@ module Gitlab :review end + def legend + "Relative Merged Requests" + end + def description "Time between merge request creation and merge/close" end diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb index 945909a4d62..3cdbe04fbaf 100644 --- a/lib/gitlab/cycle_analytics/staging_stage.rb +++ b/lib/gitlab/cycle_analytics/staging_stage.rb @@ -14,6 +14,10 @@ module Gitlab :staging end + def legend + "Relative Deployed Builds" + end + def description "From merge request merge until deploy to production" end diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb index 0079d56e0e4..e96943833bc 100644 --- a/lib/gitlab/cycle_analytics/test_stage.rb +++ b/lib/gitlab/cycle_analytics/test_stage.rb @@ -13,6 +13,10 @@ module Gitlab :test end + def legend + "Relative Builds Trigger by Commits" + end + def description "Total test time for all commits/merges" end From f193ef16c0253a0790ed8bcd339373bba62b724d Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 10 Feb 2017 03:42:26 -0500 Subject: [PATCH 349/488] Make sure events have most properties defined --- .../cycle_analytics_bundle.js.es6 | 4 +- .../cycle_analytics_store.js.es6 | 47 +++++---- .../default_event_objects.js.es6 | 98 +++++++++++++++++++ .../javascripts/lib/utils/text_utility.js | 10 +- app/assets/javascripts/wikis.js.es6 | 6 +- 5 files changed, 137 insertions(+), 28 deletions(-) create mode 100644 app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 index c41c57c1dcd..f161eb23795 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 @@ -97,7 +97,7 @@ $(() => { } this.isLoadingStage = true; - cycleAnalyticsStore.setStageEvents([]); + cycleAnalyticsStore.setStageEvents([], stage); cycleAnalyticsStore.setActiveStage(stage); cycleAnalyticsService @@ -107,7 +107,7 @@ $(() => { }) .done((response) => { this.isEmptyStage = !response.events.length; - cycleAnalyticsStore.setStageEvents(response.events); + cycleAnalyticsStore.setStageEvents(response.events, stage); }) .error(() => { this.isEmptyStage = true; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 index be732971c7f..3efeb141008 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 @@ -1,4 +1,8 @@ /* eslint-disable no-param-reassign */ + +require('../lib/utils/text_utility'); +const DEFAULT_EVENT_OBJECTS = require('./default_event_objects'); + ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; @@ -34,11 +38,12 @@ }); newData.stages.forEach((item) => { - const stageName = item.title.toLowerCase(); + const stageSlug = gl.text.dasherize(item.title.toLowerCase()); item.active = false; - item.isUserAllowed = data.permissions[stageName]; - item.emptyStageText = EMPTY_STAGE_TEXTS[stageName]; - item.component = `stage-${stageName}-component`; + item.isUserAllowed = data.permissions[stageSlug]; + item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug]; + item.component = `stage-${stageSlug}-component`; + item.slug = stageSlug; }); newData.analytics = data; return newData; @@ -58,31 +63,33 @@ this.deactivateAllStages(); stage.active = true; }, - setStageEvents(events) { - this.state.events = this.decorateEvents(events); + setStageEvents(events, stage) { + this.state.events = this.decorateEvents(events, stage); }, - decorateEvents(events) { + decorateEvents(events, stage) { const newEvents = []; events.forEach((item) => { if (!item) return; - item.totalTime = item.total_time; - item.author.webUrl = item.author.web_url; - item.author.avatarUrl = item.author.avatar_url; + const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item); - if (item.created_at) item.createdAt = item.created_at; - if (item.short_sha) item.shortSha = item.short_sha; - if (item.commit_url) item.commitUrl = item.commit_url; + eventItem.totalTime = eventItem.total_time; + eventItem.author.webUrl = eventItem.author.web_url; + eventItem.author.avatarUrl = eventItem.author.avatar_url; - delete item.author.web_url; - delete item.author.avatar_url; - delete item.total_time; - delete item.created_at; - delete item.short_sha; - delete item.commit_url; + if (eventItem.created_at) eventItem.createdAt = eventItem.created_at; + if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha; + if (eventItem.commit_url) eventItem.commitUrl = eventItem.commit_url; - newEvents.push(item); + delete eventItem.author.web_url; + delete eventItem.author.avatar_url; + delete eventItem.total_time; + delete eventItem.created_at; + delete eventItem.short_sha; + delete eventItem.commit_url; + + newEvents.push(eventItem); }); return newEvents; diff --git a/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 b/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 new file mode 100644 index 00000000000..cfaf9835bf8 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 @@ -0,0 +1,98 @@ +module.exports = { + issue: { + created_at: '', + url: '', + iid: '', + title: '', + total_time: {}, + author: { + avatar_url: '', + id: '', + name: '', + web_url: '', + }, + }, + plan: { + title: '', + commit_url: '', + short_sha: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + }, + code: { + title: '', + iid: '', + created_at: '', + url: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + }, + test: { + name: '', + id: '', + date: '', + url: '', + short_sha: '', + commit_url: '', + total_time: {}, + branch: { + name: '', + url: '', + }, + }, + review: { + title: '', + iid: '', + created_at: '', + url: '', + state: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + }, + staging: { + id: '', + short_sha: '', + date: '', + url: '', + commit_url: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + branch: { + name: '', + url: '', + }, + }, + production: { + title: '', + created_at: '', + url: '', + iid: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + }, +}; diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index d9370db0cf2..326b7cb7f57 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -1,5 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len */ +require('vendor/latinise'); + (function() { (function(w) { var base; @@ -164,8 +166,14 @@ gl.text.pluralize = function(str, count) { return str + (count > 1 || count === 0 ? 's' : ''); }; - return gl.text.truncate = function(string, maxLength) { + gl.text.truncate = function(string, maxLength) { return string.substr(0, (maxLength - 3)) + '...'; }; + gl.text.dasherize = function(str) { + return str.replace(/[_\s]+/g, '-'); + }; + gl.text.slugify = function(str) { + return str.trim().toLowerCase().latinise(); + }; })(window); }).call(this); diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6 index ef99b2e92f0..75fd1394a03 100644 --- a/app/assets/javascripts/wikis.js.es6 +++ b/app/assets/javascripts/wikis.js.es6 @@ -1,14 +1,10 @@ /* eslint-disable no-param-reassign */ /* global Breakpoints */ -require('vendor/latinise'); require('./breakpoints'); require('vendor/jquery.nicescroll'); ((global) => { - const dasherize = str => str.replace(/[_\s]+/g, '-'); - const slugify = str => dasherize(str.trim().toLowerCase().latinise()); - class Wikis { constructor() { this.bp = Breakpoints.get(); @@ -34,7 +30,7 @@ require('vendor/jquery.nicescroll'); if (!this.newWikiForm) return; const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); - const slug = slugify(slugInput.value); + const slug = gl.text.slugify(slugInput.value); if (slug.length > 0) { const wikisPath = slugInput.getAttribute('data-wikis-path'); From 9520627fa9cb6e7eae08d1ed384fd5225bd612a7 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 10 Feb 2017 12:54:16 +0000 Subject: [PATCH 350/488] Updated copy --- doc/user/project/issue_board.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 3dcdc181d3d..2ffb170cadd 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -94,11 +94,11 @@ list view that is removed. You can always add it back later if you need. ## Adding issues to lists -You can bulk add issues to a list by clicking the **Add issues** button. -This opens up a modal window where you can select multiple issues and then -add these issues to the selected list. By default the first list is selected, -but this can be changed in the dropdown menu next to the **Add issues** button -in the modal. +Add issues to a list by clicking the **Add issues** button. This opens up a +modal window where you can see all the issues that do not belong to any list. +Select one or more issues and then add these issues to the selected list. +By default the first list is selected, but this can be changed in the dropdown +menu next to the **Add issues** button in the modal. Within this modal you can also filter issues. This is done by using the filters at the top of the modal. @@ -146,7 +146,7 @@ A typical workflow of using the Issue Board would be: and gets automatically closed. For instance you can create a list based on the label of 'Frontend' and one for -'Backend'. A designer can start working on an issue by dragging adding it to the +'Backend'. A designer can start working on an issue by adding it to the 'Frontend' list. That way, everyone knows that this issue is now being worked on by the designers. Then, once they're done, all they have to do is drag it over to the next list, 'Backend', where a backend developer can From d1ecbd6ce8c3dac8228397259a3fd41cbb1577f8 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 10 Feb 2017 11:44:00 -0200 Subject: [PATCH 351/488] Fix admin_labels_spec.rb transient failure --- spec/features/admin/admin_labels_spec.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb index eaa42aad0a7..6d6c9165c83 100644 --- a/spec/features/admin/admin_labels_spec.rb +++ b/spec/features/admin/admin_labels_spec.rb @@ -35,15 +35,16 @@ RSpec.describe 'admin issues labels' do it 'deletes all labels', js: true do page.within '.labels' do page.all('.btn-remove').each do |remove| - wait_for_ajax remove.click + wait_for_ajax end end - page.within '.manage-labels-list' do - expect(page).not_to have_content('bug') - expect(page).not_to have_content('feature_label') - end + wait_for_ajax + + expect(page).to have_content("There are no labels yet") + expect(page).not_to have_content('bug') + expect(page).not_to have_content('feature_label') end end From 88d610c60e9064f92419481a9df6453b3c8079b3 Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova Date: Thu, 9 Feb 2017 13:39:39 +0100 Subject: [PATCH 352/488] Add member: Always return 409 when a member exists --- .../unreleased/20732_member_exists_409.yml | 4 + doc/api/v3_to_v4.md | 1 + lib/api/api.rb | 1 + lib/api/members.rb | 11 +- lib/api/v3/members.rb | 134 +++++++ spec/requests/api/members_spec.rb | 4 +- spec/requests/api/v3/members_spec.rb | 342 ++++++++++++++++++ 7 files changed, 486 insertions(+), 11 deletions(-) create mode 100644 changelogs/unreleased/20732_member_exists_409.yml create mode 100644 lib/api/v3/members.rb create mode 100644 spec/requests/api/v3/members_spec.rb diff --git a/changelogs/unreleased/20732_member_exists_409.yml b/changelogs/unreleased/20732_member_exists_409.yml new file mode 100644 index 00000000000..135647c7ac3 --- /dev/null +++ b/changelogs/unreleased/20732_member_exists_409.yml @@ -0,0 +1,4 @@ +--- +title: 'Add member: Always return 409 when a member exists' +merge_request: +author: diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 707f0437b7e..7cb83a337f2 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -12,3 +12,4 @@ changes are in V4: - Endpoints under `projects/merge_request/:id` have been removed (use: `projects/merge_requests/:id`) - Project snippets do not return deprecated field `expires_at` - Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`) +- Status 409 returned for POST `project/:id/members` when a member already exists diff --git a/lib/api/api.rb b/lib/api/api.rb index eb9792680ff..7ec089b9c29 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -7,6 +7,7 @@ module API version 'v3', using: :path do mount ::API::V3::DeployKeys mount ::API::V3::Issues + mount ::API::V3::Members mount ::API::V3::MergeRequests mount ::API::V3::Projects mount ::API::V3::ProjectSnippets diff --git a/lib/api/members.rb b/lib/api/members.rb index d85f1f78cd6..d1d78775c6d 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -56,16 +56,9 @@ module API member = source.members.find_by(user_id: params[:user_id]) - # We need this explicit check because `source.add_user` doesn't - # currently return the member created so it would return 201 even if - # the member already existed... - # The `source_type == 'group'` check is to ensure back-compatibility - # but 409 behavior should be used for both project and group members in 9.0! - conflict!('Member already exists') if source_type == 'group' && member + conflict!('Member already exists') if member - unless member - member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at]) - end + member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at]) if member.persisted? && member.valid? present member.user, with: Entities::Member, member: member diff --git a/lib/api/v3/members.rb b/lib/api/v3/members.rb new file mode 100644 index 00000000000..4e6cb2e3c52 --- /dev/null +++ b/lib/api/v3/members.rb @@ -0,0 +1,134 @@ +module API + module V3 + class Members < Grape::API + include PaginationParams + + before { authenticate! } + + helpers ::API::Helpers::MembersHelpers + + %w[group project].each do |source_type| + params do + requires :id, type: String, desc: "The #{source_type} ID" + end + resource source_type.pluralize do + desc 'Gets a list of group or project members viewable by the authenticated user.' do + success ::API::Entities::Member + end + params do + optional :query, type: String, desc: 'A query string to search for members' + use :pagination + end + get ":id/members" do + source = find_source(source_type, params[:id]) + + users = source.users + users = users.merge(User.search(params[:query])) if params[:query] + + present paginate(users), with: ::API::Entities::Member, source: source + end + + desc 'Gets a member of a group or project.' do + success ::API::Entities::Member + end + params do + requires :user_id, type: Integer, desc: 'The user ID of the member' + end + get ":id/members/:user_id" do + source = find_source(source_type, params[:id]) + + members = source.members + member = members.find_by!(user_id: params[:user_id]) + + present member.user, with: ::API::Entities::Member, member: member + end + + desc 'Adds a member to a group or project.' do + success ::API::Entities::Member + end + params do + requires :user_id, type: Integer, desc: 'The user ID of the new member' + requires :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)' + optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY' + end + post ":id/members" do + source = find_source(source_type, params[:id]) + authorize_admin_source!(source_type, source) + + member = source.members.find_by(user_id: params[:user_id]) + + # We need this explicit check because `source.add_user` doesn't + # currently return the member created so it would return 201 even if + # the member already existed... + # The `source_type == 'group'` check is to ensure back-compatibility + # but 409 behavior should be used for both project and group members in 9.0! + conflict!('Member already exists') if source_type == 'group' && member + + unless member + member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at]) + end + if member.persisted? && member.valid? + present member.user, with: ::API::Entities::Member, member: member + else + # This is to ensure back-compatibility but 400 behavior should be used + # for all validation errors in 9.0! + render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level) + render_validation_error!(member) + end + end + + desc 'Updates a member of a group or project.' do + success ::API::Entities::Member + end + params do + requires :user_id, type: Integer, desc: 'The user ID of the new member' + requires :access_level, type: Integer, desc: 'A valid access level' + optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY' + end + put ":id/members/:user_id" do + source = find_source(source_type, params[:id]) + authorize_admin_source!(source_type, source) + + member = source.members.find_by!(user_id: params[:user_id]) + attrs = attributes_for_keys [:access_level, :expires_at] + + if member.update_attributes(attrs) + present member.user, with: ::API::Entities::Member, member: member + else + # This is to ensure back-compatibility but 400 behavior should be used + # for all validation errors in 9.0! + render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level) + render_validation_error!(member) + end + end + + desc 'Removes a user from a group or project.' + params do + requires :user_id, type: Integer, desc: 'The user ID of the member' + end + delete ":id/members/:user_id" do + source = find_source(source_type, params[:id]) + + # This is to ensure back-compatibility but find_by! should be used + # in that casse in 9.0! + member = source.members.find_by(user_id: params[:user_id]) + + # This is to ensure back-compatibility but this should be removed in + # favor of find_by! in 9.0! + not_found!("Member: user_id:#{params[:user_id]}") if source_type == 'group' && member.nil? + + # This is to ensure back-compatibility but 204 behavior should be used + # for all DELETE endpoints in 9.0! + if member.nil? + { message: "Access revoked", id: params[:user_id].to_i } + else + ::Members::DestroyService.new(source, current_user, declared_params).execute + + present member.user, with: ::API::Entities::Member, member: member + end + end + end + end + end + end +end diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 9892e014cb9..3e9bcfd1a60 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -145,11 +145,11 @@ describe API::Members, api: true do end end - it "returns #{source_type == 'project' ? 201 : 409} if member already exists" do + it "returns 409 if member already exists" do post api("/#{source_type.pluralize}/#{source.id}/members", master), user_id: master.id, access_level: Member::MASTER - expect(response).to have_http_status(source_type == 'project' ? 201 : 409) + expect(response).to have_http_status(409) end it 'returns 400 when user_id is not given' do diff --git a/spec/requests/api/v3/members_spec.rb b/spec/requests/api/v3/members_spec.rb new file mode 100644 index 00000000000..28c3ca03960 --- /dev/null +++ b/spec/requests/api/v3/members_spec.rb @@ -0,0 +1,342 @@ +require 'spec_helper' + +describe API::Members, api: true do + include ApiHelpers + + let(:master) { create(:user) } + let(:developer) { create(:user) } + let(:access_requester) { create(:user) } + let(:stranger) { create(:user) } + + let(:project) do + create(:empty_project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project| + project.team << [developer, :developer] + project.team << [master, :master] + project.request_access(access_requester) + end + end + + let!(:group) do + create(:group, :public, :access_requestable) do |group| + group.add_developer(developer) + group.add_owner(master) + group.request_access(access_requester) + end + end + + shared_examples 'GET /:sources/:id/members' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { get v3_api("/#{source_type.pluralize}/#{source.id}/members", stranger) } + end + + %i[master developer access_requester stranger].each do |type| + context "when authenticated as a #{type}" do + it 'returns 200' do + user = public_send(type) + get v3_api("/#{source_type.pluralize}/#{source.id}/members", user) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(2) + expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] + end + end + end + + it 'does not return invitees' do + create(:"#{source_type}_member", invite_token: '123', invite_email: 'test@abc.com', source: source, user: nil) + + get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(2) + expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] + end + + it 'finds members with query string' do + get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username + + expect(response).to have_http_status(200) + expect(json_response.count).to eq(1) + expect(json_response.first['username']).to eq(master.username) + end + end + end + + shared_examples 'GET /:sources/:id/members/:user_id' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { get v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) } + end + + context 'when authenticated as a non-member' do + %i[access_requester stranger].each do |type| + context "as a #{type}" do + it 'returns 200' do + user = public_send(type) + get v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user) + + expect(response).to have_http_status(200) + # User attributes + expect(json_response['id']).to eq(developer.id) + expect(json_response['name']).to eq(developer.name) + expect(json_response['username']).to eq(developer.username) + expect(json_response['state']).to eq(developer.state) + expect(json_response['avatar_url']).to eq(developer.avatar_url) + expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(developer)) + + # Member attributes + expect(json_response['access_level']).to eq(Member::DEVELOPER) + end + end + end + end + end + end + + shared_examples 'POST /:sources/:id/members' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", stranger), + user_id: access_requester.id, access_level: Member::MASTER + end + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger developer].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + post v3_api("/#{source_type.pluralize}/#{source.id}/members", user), + user_id: access_requester.id, access_level: Member::MASTER + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + context 'and new member is already a requester' do + it 'transforms the requester into a proper member' do + expect do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: access_requester.id, access_level: Member::MASTER + + expect(response).to have_http_status(201) + end.to change { source.members.count }.by(1) + expect(source.requesters.count).to eq(0) + expect(json_response['id']).to eq(access_requester.id) + expect(json_response['access_level']).to eq(Member::MASTER) + end + end + + it 'creates a new member' do + expect do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05' + + expect(response).to have_http_status(201) + end.to change { source.members.count }.by(1) + expect(json_response['id']).to eq(stranger.id) + expect(json_response['access_level']).to eq(Member::DEVELOPER) + expect(json_response['expires_at']).to eq('2016-08-05') + end + end + + it "returns #{source_type == 'project' ? 201 : 409} if member already exists" do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: master.id, access_level: Member::MASTER + + expect(response).to have_http_status(source_type == 'project' ? 201 : 409) + end + + it 'returns 400 when user_id is not given' do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), + access_level: Member::MASTER + + expect(response).to have_http_status(400) + end + + it 'returns 400 when access_level is not given' do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: stranger.id + + expect(response).to have_http_status(400) + end + + it 'returns 422 when access_level is not valid' do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: stranger.id, access_level: 1234 + + expect(response).to have_http_status(422) + end + end + end + + shared_examples 'PUT /:sources/:id/members/:user_id' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) do + put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger), + access_level: Member::MASTER + end + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger developer].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user), + access_level: Member::MASTER + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + it 'updates the member' do + put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), + access_level: Member::MASTER, expires_at: '2016-08-05' + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(developer.id) + expect(json_response['access_level']).to eq(Member::MASTER) + expect(json_response['expires_at']).to eq('2016-08-05') + end + end + + it 'returns 409 if member does not exist' do + put v3_api("/#{source_type.pluralize}/#{source.id}/members/123", master), + access_level: Member::MASTER + + expect(response).to have_http_status(404) + end + + it 'returns 400 when access_level is not given' do + put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master) + + expect(response).to have_http_status(400) + end + + it 'returns 422 when access level is not valid' do + put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), + access_level: 1234 + + expect(response).to have_http_status(422) + end + end + end + + shared_examples 'DELETE /:sources/:id/members/:user_id' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) } + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user) + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as a member and deleting themself' do + it 'deletes the member' do + expect do + delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer) + + expect(response).to have_http_status(200) + end.to change { source.members.count }.by(-1) + end + end + + context 'when authenticated as a master/owner' do + context 'and member is a requester' do + it "returns #{source_type == 'project' ? 200 : 404}" do + expect do + delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master) + + expect(response).to have_http_status(source_type == 'project' ? 200 : 404) + end.not_to change { source.requesters.count } + end + end + + it 'deletes the member' do + expect do + delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master) + + expect(response).to have_http_status(200) + end.to change { source.members.count }.by(-1) + end + end + + it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do + delete v3_api("/#{source_type.pluralize}/#{source.id}/members/123", master) + + expect(response).to have_http_status(source_type == 'project' ? 200 : 404) + end + end + end + + it_behaves_like 'GET /:sources/:id/members', 'project' do + let(:source) { project } + end + + it_behaves_like 'GET /:sources/:id/members', 'group' do + let(:source) { group } + end + + it_behaves_like 'GET /:sources/:id/members/:user_id', 'project' do + let(:source) { project } + end + + it_behaves_like 'GET /:sources/:id/members/:user_id', 'group' do + let(:source) { group } + end + + it_behaves_like 'POST /:sources/:id/members', 'project' do + let(:source) { project } + end + + it_behaves_like 'POST /:sources/:id/members', 'group' do + let(:source) { group } + end + + it_behaves_like 'PUT /:sources/:id/members/:user_id', 'project' do + let(:source) { project } + end + + it_behaves_like 'PUT /:sources/:id/members/:user_id', 'group' do + let(:source) { group } + end + + it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'project' do + let(:source) { project } + end + + it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'group' do + let(:source) { group } + end + + context 'Adding owner to project' do + it 'returns 403' do + expect do + post v3_api("/projects/#{project.id}/members", master), + user_id: stranger.id, access_level: Member::OWNER + + expect(response).to have_http_status(422) + end.to change { project.members.count }.by(0) + end + end +end From d731905dbc676f25bcedc61dd8fff86be37e553c Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 10 Feb 2017 14:56:40 +0000 Subject: [PATCH 353/488] Updated protected branches dropdown image in docs --- .../img/protected_branches_devs_can_push.png | Bin 8302 -> 34888 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/user/project/img/protected_branches_devs_can_push.png b/doc/user/project/img/protected_branches_devs_can_push.png index 1c05cb8fd36011094fca6d84f508f48abb6af03f..320e6eb7feec951aaf2b00c1df8fe6b5c786f5c8 100644 GIT binary patch literal 34888 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 L4K*t@AQArqp)-5% From 6b762b9af7dcb09cbd4c5a3eee1164e31255a7a3 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 10 Feb 2017 15:27:45 +0000 Subject: [PATCH 354/488] Fix tooltip scss for anchors. Adds css for button's tooltips to have similar behavior. Removes padding from mini graph table cell to guarantee stages don't break line on hover --- app/assets/stylesheets/pages/pipelines.scss | 14 ++++++++++---- changelogs/unreleased/27963-tooltips-jobs.yml | 4 ++++ 2 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/27963-tooltips-jobs.yml diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 974100bdff0..0c7019dc64f 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -94,6 +94,10 @@ padding: 10px 8px; } + td.stage-cell { + padding: 10px 0; + } + .commit-link { padding: 9px 8px 10px; } @@ -291,12 +295,14 @@ height: 22px; margin: 3px 6px 3px 0; - .tooltip { - white-space: nowrap; + // Hack to show a button tooltip inline + button.has-tooltip + .tooltip { + min-width: 105px; } - .tooltip-inner { - padding: 3px 4px; + // Bootstrap way of showing the content inline for anchors. + a.has-tooltip { + white-space: nowrap; } &:not(:last-child) { diff --git a/changelogs/unreleased/27963-tooltips-jobs.yml b/changelogs/unreleased/27963-tooltips-jobs.yml new file mode 100644 index 00000000000..ba418d86433 --- /dev/null +++ b/changelogs/unreleased/27963-tooltips-jobs.yml @@ -0,0 +1,4 @@ +--- +title: Fix tooltips in mini pipeline graph +merge_request: +author: From 5f85487c1526f2921f1cef30aceb2fddf84d3632 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 8 Feb 2017 17:02:25 +0200 Subject: [PATCH 355/488] Show parent group members for nested group Signed-off-by: Dmitriy Zaporozhets --- .../groups/group_members_controller.rb | 2 +- app/finders/group_members_finder.rb | 20 ++++++++++ app/models/group.rb | 2 +- app/models/member.rb | 1 + app/views/shared/members/_member.html.haml | 7 ++-- spec/features/groups/members/list_spec.rb | 40 +++++++++++++++++++ spec/finders/group_members_finder_spec.rb | 32 +++++++++++++++ spec/models/member_spec.rb | 8 ++++ 8 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 app/finders/group_members_finder.rb create mode 100644 spec/features/groups/members/list_spec.rb create mode 100644 spec/finders/group_members_finder_spec.rb diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 4f273a8d4f0..0cbf3eb58a3 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -9,7 +9,7 @@ class Groups::GroupMembersController < Groups::ApplicationController @sort = params[:sort].presence || sort_value_name @project = @group.projects.find(params[:project_id]) if params[:project_id] - @members = @group.group_members + @members = GroupMembersFinder.new(@group).execute @members = @members.non_invite unless can?(current_user, :admin_group, @group) @members = @members.search(params[:search]) if params[:search].present? @members = @members.sort(@sort) diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb new file mode 100644 index 00000000000..9f2206346ce --- /dev/null +++ b/app/finders/group_members_finder.rb @@ -0,0 +1,20 @@ +class GroupMembersFinder < Projects::ApplicationController + def initialize(group) + @group = group + end + + def execute + group_members = @group.members + + return group_members unless @group.parent + + parents_members = GroupMember.non_request. + where(source_id: @group.ancestors.select(:id)). + where.not(user_id: @group.users.select(:id)) + + wheres = ["members.id IN (#{group_members.select(:id).to_sql})"] + wheres << "members.id IN (#{parents_members.select(:id).to_sql})" + + GroupMember.where(wheres.join(' OR ')) + end +end diff --git a/app/models/group.rb b/app/models/group.rb index a5b92283daa..cc6624ff4aa 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -206,7 +206,7 @@ class Group < Namespace end def members_with_parents - GroupMember.where(requested_at: nil, source_id: ancestors.map(&:id).push(id)) + GroupMember.non_request.where(source_id: ancestors.map(&:id).push(id)) end def users_with_parents diff --git a/app/models/member.rb b/app/models/member.rb index 26a6054e00d..d07f270b757 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -47,6 +47,7 @@ class Member < ActiveRecord::Base scope :invite, -> { where.not(invite_token: nil) } scope :non_invite, -> { where(invite_token: nil) } scope :request, -> { where.not(requested_at: nil) } + scope :non_request, -> { where(requested_at: nil) } scope :has_access, -> { active.where('access_level > 0') } diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 659d4c905fc..239387fc9fa 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -19,9 +19,9 @@ %label.label.label-danger %strong Blocked - - if source.instance_of?(Group) && !@group + - if source.instance_of?(Group) && source != @group · - = link_to source.name, source, class: "member-group-link" + = link_to source.full_name, source, class: "member-group-link" .hidden-xs.cgray - if member.request? @@ -44,8 +44,9 @@ = link_to member.created_by.name, user_path(member.created_by) = time_ago_with_tooltip(member.created_at) - if show_roles + - current_resource = @project || @group .controls.member-controls - - if show_controls && (member.respond_to?(:group) && @group) || (member.respond_to?(:project) && @project) + - if show_controls && member.source == current_resource - if user != current_user = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| = f.hidden_field :access_level diff --git a/spec/features/groups/members/list_spec.rb b/spec/features/groups/members/list_spec.rb new file mode 100644 index 00000000000..109de39b2dd --- /dev/null +++ b/spec/features/groups/members/list_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +feature 'Groups members list', feature: true do + let(:user1) { create(:user, name: 'John Doe') } + let(:user2) { create(:user, name: 'Mary Jane') } + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + + background do + login_as(user1) + end + + scenario 'show members from current group and parent' do + group.add_developer(user1) + nested_group.add_developer(user2) + + visit group_group_members_path(nested_group) + + expect(first_row.text).to include(user1.name) + expect(second_row.text).to include(user2.name) + end + + scenario 'show user once if member of both current group and parent' do + group.add_developer(user1) + nested_group.add_developer(user1) + + visit group_group_members_path(nested_group) + + expect(first_row.text).to include(user1.name) + expect(second_row).to be_blank + end + + def first_row + page.all('ul.content-list > li')[0] + end + + def second_row + page.all('ul.content-list > li')[1] + end +end diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb new file mode 100644 index 00000000000..b762756f9ce --- /dev/null +++ b/spec/finders/group_members_finder_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe GroupMembersFinder, '#execute' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, :access_requestable, parent: group) } + let(:user1) { create(:user) } + let(:user2) { create(:user) } + let(:user3) { create(:user) } + let(:user4) { create(:user) } + + it 'returns members for top-level group' do + member1 = group.add_master(user1) + member2 = group.add_master(user2) + member3 = group.add_master(user3) + + result = described_class.new(group).execute + + expect(result.to_a).to eq([member3, member2, member1]) + end + + it 'returns members for nested group' do + group.add_master(user2) + nested_group.request_access(user4) + member1 = group.add_master(user1) + member3 = nested_group.add_master(user2) + member4 = nested_group.add_master(user3) + + result = described_class.new(nested_group).execute + + expect(result.to_a).to eq([member4, member3, member1]) + end +end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 16e2144d6a1..c720cc9f2c2 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -129,6 +129,14 @@ describe Member, models: true do it { expect(described_class.request).not_to include @accepted_request_member } end + describe '.non_request' do + it { expect(described_class.non_request).to include @master } + it { expect(described_class.non_request).to include @invited_member } + it { expect(described_class.non_request).to include @accepted_invite_member } + it { expect(described_class.non_request).not_to include @requested_member } + it { expect(described_class.non_request).to include @accepted_request_member } + end + describe '.developers' do subject { described_class.developers.to_a } From fd38397027ff7a22915e245fad47959bb2b73f4d Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Fri, 10 Feb 2017 13:31:06 +0000 Subject: [PATCH 356/488] rspec_profiling: Discover the correct branch name in GitLab CI --- config/initializers/rspec_profiling.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/config/initializers/rspec_profiling.rb b/config/initializers/rspec_profiling.rb index f462e654b2c..0ef9f51e5cf 100644 --- a/config/initializers/rspec_profiling.rb +++ b/config/initializers/rspec_profiling.rb @@ -4,6 +4,12 @@ module RspecProfilingConnection end end +module RspecProfilingGitBranchCi + def branch + ENV['CI_BUILD_REF_NAME'] || super + end +end + if Rails.env.test? RspecProfiling.configure do |config| if ENV['RSPEC_PROFILING_POSTGRES_URL'] @@ -11,4 +17,6 @@ if Rails.env.test? config.collector = RspecProfiling::Collectors::PSQL end end + + RspecProfiling::VCS::Git.prepend(RspecProfilingGitBranchCi) if ENV.has_key?('CI') end From 41d7b47c3cd3357269f967548428a768a606f0f0 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 10 Feb 2017 00:01:23 -0800 Subject: [PATCH 357/488] Add index to ci_trigger_requests for commit_id https://gitlab.com/gitlab-org/gitlab-ce/pipelines.json makes a number of unindexed slow queries. This index should speed things up. --- .../sh-add-index-to-ci-trigger-requests.yml | 4 ++++ ..._add_index_to_ci_trigger_requests_for_commit_id.rb | 11 +++++++++++ db/schema.rb | 4 +++- 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml create mode 100644 db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb diff --git a/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml b/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml new file mode 100644 index 00000000000..bab76812a17 --- /dev/null +++ b/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml @@ -0,0 +1,4 @@ +--- +title: Add index to ci_trigger_requests for commit_id +merge_request: +author: diff --git a/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb new file mode 100644 index 00000000000..61e49c14fc0 --- /dev/null +++ b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb @@ -0,0 +1,11 @@ +class AddIndexToCiTriggerRequestsForCommitId < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_concurrent_index :ci_trigger_requests, :commit_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 3fef5b82073..d71911eaf14 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: 20170206101030) do +ActiveRecord::Schema.define(version: 20170210075922) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -367,6 +367,8 @@ ActiveRecord::Schema.define(version: 20170206101030) do t.integer "commit_id" end + add_index "ci_trigger_requests", ["commit_id"], name: "index_ci_trigger_requests_on_commit_id", using: :btree + create_table "ci_triggers", force: :cascade do |t| t.string "token" t.integer "project_id" From 0260f92db4260ff655af2c5a39f4238a33f306d6 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Fri, 10 Feb 2017 08:43:01 -0600 Subject: [PATCH 358/488] Remove orange caret icon from mr widget --- app/assets/stylesheets/pages/merge_requests.scss | 7 ------- .../unreleased/27991-success-with-warnings-caret.yml | 4 ++++ 2 files changed, 4 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/27991-success-with-warnings-caret.yml diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 692142c5887..1431673027f 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -96,13 +96,6 @@ padding-right: 4px; } - &.ci-success_with_warnings { - - i { - color: $gl-warning; - } - } - @media (max-width: $screen-xs-max) { flex-wrap: wrap; } diff --git a/changelogs/unreleased/27991-success-with-warnings-caret.yml b/changelogs/unreleased/27991-success-with-warnings-caret.yml new file mode 100644 index 00000000000..703d34a5ede --- /dev/null +++ b/changelogs/unreleased/27991-success-with-warnings-caret.yml @@ -0,0 +1,4 @@ +--- +title: Fix icon colors in merge request widget mini graph +merge_request: +author: From 71cc5de594e4a8657ae1465e6e261ec09898b95b Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Fri, 10 Feb 2017 09:49:17 -0600 Subject: [PATCH 359/488] Only show MR widget graph if there are stages --- app/views/projects/merge_requests/widget/_heading.html.haml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index bef76f16ca7..e3062f47788 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -9,8 +9,9 @@ Pipeline = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline' = ci_label_for_status(status) - .mr-widget-pipeline-graph - = render 'shared/mini_pipeline_graph', pipeline: @pipeline, klass: 'js-pipeline-inline-mr-widget-graph' + - if @pipeline.stages.any? + .mr-widget-pipeline-graph + = render 'shared/mini_pipeline_graph', pipeline: @pipeline, klass: 'js-pipeline-inline-mr-widget-graph' %span for = succeed "." do From ce9a9b67aa7a33b63c55a237bdb276a7294052b1 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Fri, 10 Feb 2017 10:03:38 -0600 Subject: [PATCH 360/488] Add changelog --- changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml diff --git a/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml b/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml new file mode 100644 index 00000000000..e4287d6276c --- /dev/null +++ b/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml @@ -0,0 +1,4 @@ +--- +title: Show pipeline graph in MR widget if there are any stages +merge_request: +author: From 49c4059a3256969696a7d96d70ce78731fb1a406 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 6 Feb 2017 19:42:39 -0600 Subject: [PATCH 361/488] Replace teaspoon references with Karma --- doc/development/frontend.md | 18 ++++++------------ doc/development/rake_tasks.md | 10 +++++----- doc/development/testing.md | 7 +++---- .../javascripts/behaviors/quick_submit_spec.js | 2 +- 4 files changed, 15 insertions(+), 22 deletions(-) diff --git a/doc/development/frontend.md b/doc/development/frontend.md index 75fdf3d8e63..91f5b571ce0 100644 --- a/doc/development/frontend.md +++ b/doc/development/frontend.md @@ -250,23 +250,17 @@ information. ### Running frontend tests -`rake teaspoon` runs the frontend-only (JavaScript) tests. +`rake karma` runs the frontend-only (JavaScript) tests. It consists of two subtasks: -- `rake teaspoon:fixtures` (re-)generates fixtures -- `rake teaspoon:tests` actually executes the tests +- `rake karma:fixtures` (re-)generates fixtures +- `rake karma:tests` actually executes the tests -As long as the fixtures don't change, `rake teaspoon:tests` is sufficient +As long as the fixtures don't change, `rake karma:tests` is sufficient (and saves you some time). -If you need to debug your tests and/or application code while they're -running, navigate to [localhost:3000/teaspoon](http://localhost:3000/teaspoon) -in your browser, open DevTools, and run tests for individual files by clicking -on them. This is also much faster than setting up and running tests from the -command line. - Please note: Not all of the frontend fixtures are generated. Some are still static -files. These will not be touched by `rake teaspoon:fixtures`. +files. These will not be touched by `rake karma:fixtures`. ## Design Patterns @@ -370,7 +364,7 @@ For our currently-supported browsers, see our [requirements][requirements]. ### Spec errors due to use of ES6 features in `.js` files If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being -thrown in Teaspoon, Spinach, or Rspec tests but can't reproduce them manually, +thrown in Karma, Spinach, or Rspec tests but can't reproduce them manually, you may have included `ES6`-style JavaScript in files that don't have the `.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file you're working in (`git mv `). diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md index 827db7e99b8..dcd978c4cd3 100644 --- a/doc/development/rake_tasks.md +++ b/doc/development/rake_tasks.md @@ -17,14 +17,14 @@ Note: `db:setup` calls `db:seed` but this does nothing. In order to run the test you can use the following commands: - `rake spinach` to run the spinach suite - `rake spec` to run the rspec suite -- `rake teaspoon` to run the teaspoon test suite +- `rake karma` to run the karma test suite - `rake gitlab:test` to run all the tests -Note: Both `rake spinach` and `rake spec` takes significant time to pass. +Note: Both `rake spinach` and `rake spec` takes significant time to pass. Instead of running full test suite locally you can save a lot of time by running -a single test or directory related to your changes. After you submit merge request -CI will run full test suite for you. Green CI status in the merge request means -full test suite is passed. +a single test or directory related to your changes. After you submit merge request +CI will run full test suite for you. Green CI status in the merge request means +full test suite is passed. Note: You can't run `rspec .` since this will try to run all the `_spec.rb` files it can find, also the ones in `/tmp` diff --git a/doc/development/testing.md b/doc/development/testing.md index dbea6b9c9aa..761847b2bab 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -31,9 +31,8 @@ GitLab uses [factory_girl] as a test fixture replacement. ## JavaScript -GitLab uses [Teaspoon] to run its [Jasmine] JavaScript specs. They can be run on -the command line via `bundle exec teaspoon`, or via a web browser at -`http://localhost:3000/teaspoon` when the Rails server is running. +GitLab uses [Karma] to run its [Jasmine] JavaScript specs. They can be run on +the command line via `bundle exec karma`. - JavaScript tests live in `spec/javascripts/`, matching the folder structure of `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.es6` has a corresponding @@ -51,7 +50,7 @@ the command line via `bundle exec teaspoon`, or via a web browser at [`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification), which will have to be stubbed. -[Teaspoon]: https://github.com/modeset/teaspoon +[Karma]: https://github.com/karma-runner/karma [Jasmine]: https://github.com/jasmine/jasmine ## RSpec diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index b84126c0e3d..1541037888f 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -40,7 +40,7 @@ require('~/behaviors/quick_submit'); expect($('input[type=submit]')).toBeDisabled(); return expect($('button[type=submit]')).toBeDisabled(); }); - // We cannot stub `navigator.userAgent` for CI's `rake teaspoon` task, so we'll + // We cannot stub `navigator.userAgent` for CI's `rake karma` task, so we'll // only run the tests that apply to the current platform if (navigator.userAgent.match(/Macintosh/)) { it('responds to Meta+Enter', function() { From 191bcb4d1b5e983583e183d8945f638604c7f0e1 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 1 Feb 2017 13:21:28 -0500 Subject: [PATCH 362/488] Don't perform Devise trackable updates on blocked User records --- app/controllers/application_controller.rb | 17 +---- .../explore/application_controller.rb | 2 +- app/controllers/help_controller.rb | 2 +- app/controllers/koding_controller.rb | 2 +- .../projects/uploads_controller.rb | 4 +- app/controllers/search_controller.rb | 2 +- app/models/user.rb | 9 +++ .../unreleased/rs-warden-blocked-users.yml | 4 ++ .../projects/uploads_controller_spec.rb | 64 +++---------------- spec/factories/users.rb | 8 +++ spec/features/login_spec.rb | 16 +++++ 11 files changed, 54 insertions(+), 76 deletions(-) create mode 100644 changelogs/unreleased/rs-warden-blocked-users.yml diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bb47e2a8bf7..bf6be3d516b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,7 +12,6 @@ class ApplicationController < ActionController::Base before_action :authenticate_user_from_private_token! before_action :authenticate_user! before_action :validate_user_service_ticket! - before_action :reject_blocked! before_action :check_password_expiration before_action :check_2fa_requirement before_action :ldap_security_check @@ -87,22 +86,8 @@ class ApplicationController < ActionController::Base logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}" end - def reject_blocked! - if current_user && current_user.blocked? - sign_out current_user - flash[:alert] = "Your account is blocked. Retry when an admin has unblocked it." - redirect_to new_user_session_path - end - end - def after_sign_in_path_for(resource) - if resource.is_a?(User) && resource.respond_to?(:blocked?) && resource.blocked? - sign_out resource - flash[:alert] = "Your account is blocked. Retry when an admin has unblocked it." - new_user_session_path - else - stored_location_for(:redirect) || stored_location_for(resource) || root_path - end + stored_location_for(:redirect) || stored_location_for(resource) || root_path end def after_sign_out_path_for(resource) diff --git a/app/controllers/explore/application_controller.rb b/app/controllers/explore/application_controller.rb index a1ab8b99048..baf54520b9c 100644 --- a/app/controllers/explore/application_controller.rb +++ b/app/controllers/explore/application_controller.rb @@ -1,5 +1,5 @@ class Explore::ApplicationController < ApplicationController - skip_before_action :authenticate_user!, :reject_blocked! + skip_before_action :authenticate_user! layout 'explore' end diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 37feff79999..87c0f8905ff 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -1,5 +1,5 @@ class HelpController < ApplicationController - skip_before_action :authenticate_user!, :reject_blocked! + skip_before_action :authenticate_user! layout 'help' diff --git a/app/controllers/koding_controller.rb b/app/controllers/koding_controller.rb index f3759b4c0ea..6b1e64ce819 100644 --- a/app/controllers/koding_controller.rb +++ b/app/controllers/koding_controller.rb @@ -1,5 +1,5 @@ class KodingController < ApplicationController - before_action :check_integration!, :authenticate_user!, :reject_blocked! + before_action :check_integration! layout 'koding' def index diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb index 50ba33ed570..61686499bd3 100644 --- a/app/controllers/projects/uploads_controller.rb +++ b/app/controllers/projects/uploads_controller.rb @@ -1,6 +1,6 @@ class Projects::UploadsController < Projects::ApplicationController - skip_before_action :reject_blocked!, :project, - :repository, if: -> { action_name == 'show' && image_or_video? } + skip_before_action :project, :repository, + if: -> { action_name == 'show' && image_or_video? } before_action :authorize_upload_file!, only: [:create] diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 6576ebd5235..612d69cf557 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,5 +1,5 @@ class SearchController < ApplicationController - skip_before_action :authenticate_user!, :reject_blocked! + skip_before_action :authenticate_user! include SearchHelper diff --git a/app/models/user.rb b/app/models/user.rb index 33666b4f35b..f9245cdb82a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -167,6 +167,15 @@ class User < ActiveRecord::Base def blocked? true end + + def active_for_authentication? + false + end + + def inactive_message + "Your account has been blocked. Please contact your GitLab " \ + "administrator if you think this is an error." + end end end diff --git a/changelogs/unreleased/rs-warden-blocked-users.yml b/changelogs/unreleased/rs-warden-blocked-users.yml new file mode 100644 index 00000000000..c0c23fb6f11 --- /dev/null +++ b/changelogs/unreleased/rs-warden-blocked-users.yml @@ -0,0 +1,4 @@ +--- +title: Don't perform Devise trackable updates on blocked User records +merge_request: 8915 +author: diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb index f1c8891e87a..0347e789576 100644 --- a/spec/controllers/projects/uploads_controller_spec.rb +++ b/spec/controllers/projects/uploads_controller_spec.rb @@ -170,68 +170,24 @@ describe Projects::UploadsController do project.team << [user, :master] end - context "when the user is blocked" do + context "when the file exists" do before do - user.block - project.team << [user, :master] + allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) + allow(jpg).to receive(:exists?).and_return(true) end - context "when the file exists" do - before do - allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) - allow(jpg).to receive(:exists?).and_return(true) - end + it "responds with status 200" do + go - context "when the file is an image" do - before do - allow_any_instance_of(FileUploader).to receive(:image?).and_return(true) - end - - it "responds with status 200" do - go - - expect(response).to have_http_status(200) - end - end - - context "when the file is not an image" do - it "redirects to the sign in page" do - go - - expect(response).to redirect_to(new_user_session_path) - end - end - end - - context "when the file doesn't exist" do - it "redirects to the sign in page" do - go - - expect(response).to redirect_to(new_user_session_path) - end + expect(response).to have_http_status(200) end end - context "when the user isn't blocked" do - context "when the file exists" do - before do - allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) - allow(jpg).to receive(:exists?).and_return(true) - end + context "when the file doesn't exist" do + it "responds with status 404" do + go - it "responds with status 200" do - go - - expect(response).to have_http_status(200) - end - end - - context "when the file doesn't exist" do - it "responds with status 404" do - go - - expect(response).to have_http_status(404) - end + expect(response).to have_http_status(404) end end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index c6f7869516e..1732b1a0081 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -14,6 +14,14 @@ FactoryGirl.define do admin true end + trait :blocked do + after(:build) { |user, _| user.block! } + end + + trait :external do + external true + end + trait :two_factor do two_factor_via_otp end diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index ab7d89306db..ae609160e18 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -32,6 +32,22 @@ feature 'Login', feature: true do end end + describe 'with a blocked account' do + it 'prevents the user from logging in' do + user = create(:user, :blocked) + + login_with(user) + + expect(page).to have_content('Your account has been blocked.') + end + + it 'does not update Devise trackable attributes' do + user = create(:user, :blocked) + + expect { login_with(user) }.not_to change { user.reload.sign_in_count } + end + end + describe 'with two-factor authentication' do def enter_code(code) fill_in 'user_otp_attempt', with: code From 6bf04498f252a7aa9789a2182f61a3ea92fe0b2a Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Thu, 9 Feb 2017 19:35:52 +0000 Subject: [PATCH 363/488] Added env external link and light web terminal spec --- .../projects/environments/terminal.html.haml | 2 ++ ...add-environment-url-link-to-terminal-page.yml | 4 ++++ spec/features/environment_spec.rb | 16 ++++++++++++++++ 3 files changed, 22 insertions(+) create mode 100644 changelogs/unreleased/27336-add-environment-url-link-to-terminal-page.yml diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml index 1d49e9cbaf7..ef0dd0eda3c 100644 --- a/app/views/projects/environments/terminal.html.haml +++ b/app/views/projects/environments/terminal.html.haml @@ -16,6 +16,8 @@ .col-sm-6 .nav-controls + = link_to @environment.external_url, class: 'btn btn-default' do + = icon('external-link') = render 'projects/deployments/actions', deployment: @environment.last_deployment .terminal-container{ class: container_class } diff --git a/changelogs/unreleased/27336-add-environment-url-link-to-terminal-page.yml b/changelogs/unreleased/27336-add-environment-url-link-to-terminal-page.yml new file mode 100644 index 00000000000..dd4907166c4 --- /dev/null +++ b/changelogs/unreleased/27336-add-environment-url-link-to-terminal-page.yml @@ -0,0 +1,4 @@ +--- +title: Added external environment link to web terminal view +merge_request: 8303 +author: diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb index 2f49e89b4e4..c203e1f20c1 100644 --- a/spec/features/environment_spec.rb +++ b/spec/features/environment_spec.rb @@ -101,6 +101,22 @@ feature 'Environment', :feature do scenario 'it shows the terminal button' do expect(page).to have_terminal_button end + + context 'web terminal', :js do + before do + # Stub #terminals as it causes js-enabled feature specs to render the page incorrectly + allow_any_instance_of(Environment).to receive(:terminals) { nil } + visit terminal_namespace_project_environment_path(project.namespace, project, environment) + end + + it 'displays a web terminal' do + expect(page).to have_selector('#terminal') + end + + it 'displays a link to the environment external url' do + expect(page).to have_link(nil, href: environment.external_url) + end + end end context 'for developer' do From 8d207c6d89d6365c14a43c7f361cfd0192bd3044 Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Fri, 10 Feb 2017 23:21:20 +0600 Subject: [PATCH 364/488] fixes MR widget jump --- app/assets/javascripts/merge_request.js | 8 ++++---- app/views/shared/_commit_message_container.html.haml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 8762ec35b80..e65378cd610 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -115,8 +115,8 @@ require('./merge_request_tabs'); e.preventDefault(); textarea.val(textarea.data('messageWithDescription')); - $('p.js-with-description-hint').hide(); - $('p.js-without-description-hint').show(); + $('.js-with-description-hint').hide(); + $('.js-without-description-hint').show(); }); $(document).on('click', 'a.js-without-description-link', function(e) { @@ -124,8 +124,8 @@ require('./merge_request_tabs'); e.preventDefault(); textarea.val(textarea.data('messageWithoutDescription')); - $('p.js-with-description-hint').show(); - $('p.js-without-description-hint').hide(); + $('.js-with-description-hint').show(); + $('.js-without-description-hint').hide(); }); }; diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index c196bc06b17..4b98ff88241 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -17,9 +17,9 @@ Try to keep the first line under 52 characters and the others under 72. - if descriptions.present? - %p.hint.js-with-description-hint + .hint.js-with-description-hint = link_to "#", class: "js-with-description-link" do Include description in commit message - %p.hint.js-without-description-hint.hide + .hint.js-without-description-hint.hide = link_to "#", class: "js-without-description-link" do Don't include description in commit message From fa2339641f52688ba2465f4c602c1de36fe5b353 Mon Sep 17 00:00:00 2001 From: dixpac Date: Thu, 9 Feb 2017 20:32:09 +0100 Subject: [PATCH 365/488] Rename Files::DeleteService to Files::DestroyService Reason for renaming is to comply with naming convention of services in codebase. --- app/controllers/projects/blob_controller.rb | 8 ++++---- .../files/{delete_service.rb => destroy_service.rb} | 2 +- changelogs/unreleased/rename_files_delete_service.yml | 4 ++++ lib/api/files.rb | 2 +- spec/lib/gitlab/diff/position_tracer_spec.rb | 2 +- 5 files changed, 11 insertions(+), 7 deletions(-) rename app/services/files/{delete_service.rb => destroy_service.rb} (88%) create mode 100644 changelogs/unreleased/rename_files_delete_service.yml diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 4c39fe98028..a1db856dcfb 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -61,10 +61,10 @@ class Projects::BlobController < Projects::ApplicationController end def destroy - create_commit(Files::DeleteService, success_notice: "The file has been successfully deleted.", - success_path: namespace_project_tree_path(@project.namespace, @project, @target_branch), - failure_view: :show, - failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) + create_commit(Files::DestroyService, success_notice: "The file has been successfully deleted.", + success_path: namespace_project_tree_path(@project.namespace, @project, @target_branch), + failure_view: :show, + failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) end def diff diff --git a/app/services/files/delete_service.rb b/app/services/files/destroy_service.rb similarity index 88% rename from app/services/files/delete_service.rb rename to app/services/files/destroy_service.rb index 50f0ffcac9f..c3be806a42d 100644 --- a/app/services/files/delete_service.rb +++ b/app/services/files/destroy_service.rb @@ -1,5 +1,5 @@ module Files - class DeleteService < Files::BaseService + class DestroyService < Files::BaseService def commit repository.remove_file( current_user, diff --git a/changelogs/unreleased/rename_files_delete_service.yml b/changelogs/unreleased/rename_files_delete_service.yml new file mode 100644 index 00000000000..4de1c5b0d63 --- /dev/null +++ b/changelogs/unreleased/rename_files_delete_service.yml @@ -0,0 +1,4 @@ +--- +title: Rename Files::DeleteService to Files::DestroyService +merge_request: 9110 +author: dixpac diff --git a/lib/api/files.rb b/lib/api/files.rb index c58472de578..2ecdd747c8e 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -117,7 +117,7 @@ module API authorize! :push_code, user_project file_params = declared_params(include_missing: false) - result = ::Files::DeleteService.new(user_project, current_user, commit_params(file_params)).execute + result = ::Files::DestroyService.new(user_project, current_user, commit_params(file_params)).execute if result[:status] == :success status(200) diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index 8e3e4034c8f..994995b57b8 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -122,7 +122,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do end def delete_file(branch_name, file_name) - Files::DeleteService.new( + Files::DestroyService.new( project, current_user, start_branch: branch_name, From 0335619aacb0a0247aefdcae0378b3ff82adc4b9 Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Fri, 10 Feb 2017 23:35:12 +0600 Subject: [PATCH 366/488] adds changelog --- changelogs/unreleased/27994-fix-mr-widget-jump.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/27994-fix-mr-widget-jump.yml diff --git a/changelogs/unreleased/27994-fix-mr-widget-jump.yml b/changelogs/unreleased/27994-fix-mr-widget-jump.yml new file mode 100644 index 00000000000..77783e54a3a --- /dev/null +++ b/changelogs/unreleased/27994-fix-mr-widget-jump.yml @@ -0,0 +1,4 @@ +--- +title: Fix MR widget jump +merge_request: 9146 +author: From fd2e3da05ad2cca9e65c7ebb85bdc148d1416936 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 10 Feb 2017 18:13:15 +0000 Subject: [PATCH 367/488] Fix job to pipeline renaming --- app/views/projects/ci/pipelines/_pipeline.html.haml | 4 ++-- app/views/projects/merge_requests/widget/_show.html.haml | 4 ++-- .../merge_requests/widget/open/_build_failed.html.haml | 2 +- app/views/projects/triggers/_index.html.haml | 4 ++-- changelogs/unreleased/fix-job-to-pipeline-renaming.yml | 4 ++++ 5 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/fix-job-to-pipeline-renaming.yml diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index ac0fd87fd8d..f852f2e3fd7 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -15,7 +15,7 @@ - else %span.api.monospace API - if pipeline.latest? - %span.label.label-success.has-tooltip{ title: 'Latest job for this branch' } latest + %span.label.label-success.has-tooltip{ title: 'Latest pipeline for this branch' } latest - if pipeline.triggered? %span.label.label-primary triggered - if pipeline.yaml_errors.present? @@ -61,7 +61,7 @@ .btn-group.inline - if actions.any? .btn-group - %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual job', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual job' } + %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual pipeline', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual pipeline' } = custom_icon('icon_play') = icon('caret-down', 'aria-hidden' => 'true') %ul.dropdown-menu.dropdown-menu-align-right diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 4c063747857..0b0fb7854c2 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -17,11 +17,11 @@ ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}", ci_message: { normal: "Pipeline {{status}} for \"{{title}}\"", - preparing: "{{status}} job for \"{{title}}\"" + preparing: "{{status}} pipeline for \"{{title}}\"" }, ci_enable: #{@project.ci_service ? "true" : "false"}, ci_title: { - preparing: "{{status}} job", + preparing: "{{status}} pipeline", normal: "Pipeline {{status}}" }, ci_sha: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.short_sha : ''}", diff --git a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml index a18c2ad768f..3979d5fa8ed 100644 --- a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml +++ b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml @@ -1,6 +1,6 @@ %h4 = icon('exclamation-triangle') - The job for this merge request failed + The pipeline for this merge request failed %p Please retry the job or push a new commit to fix the failure. diff --git a/app/views/projects/triggers/_index.html.haml b/app/views/projects/triggers/_index.html.haml index 5cb1818ae54..33883facf9b 100644 --- a/app/views/projects/triggers/_index.html.haml +++ b/app/views/projects/triggers/_index.html.haml @@ -65,7 +65,7 @@ In the %code .gitlab-ci.yml of another project, include the following snippet. - The project will be rebuilt at the end of the job. + The project will be rebuilt at the end of the pipeline. %pre :plain @@ -89,7 +89,7 @@ %p.light Add %code variables[VARIABLE]=VALUE - to an API request. Variable values can be used to distinguish between triggered jobs and normal jobs. + to an API request. Variable values can be used to distinguish between triggered pipelines and normal pipelines. With cURL: diff --git a/changelogs/unreleased/fix-job-to-pipeline-renaming.yml b/changelogs/unreleased/fix-job-to-pipeline-renaming.yml new file mode 100644 index 00000000000..d5f34b4b25d --- /dev/null +++ b/changelogs/unreleased/fix-job-to-pipeline-renaming.yml @@ -0,0 +1,4 @@ +--- +title: Fix job to pipeline renaming +merge_request: 9147 +author: From 18455939ca42f1aeff6d524b046e392289877458 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 10 Feb 2017 18:54:40 +0000 Subject: [PATCH 368/488] Fix broken test to use trigger in order to not take tooltip overlaping in consideration --- spec/features/merge_requests/mini_pipeline_graph_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb index b08bd36bde9..84ad8765d8f 100644 --- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb +++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb @@ -66,7 +66,7 @@ feature 'Mini Pipeline Graph', :js, :feature do end it 'should close when toggle is clicked again' do - toggle.click + toggle.trigger('click') expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') end From 293a54a9dbfc02d685296c1872199dece624f901 Mon Sep 17 00:00:00 2001 From: Mark Pundsack Date: Fri, 10 Feb 2017 13:49:35 -0600 Subject: [PATCH 369/488] Add image description --- doc/ci/pipelines.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index 35a80dd2977..5700e9bb026 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -33,7 +33,7 @@ Pipelines accommodate several development workflows: Example continuous delivery flow: -![](img/pipelines-goal.svg) +![CD Flow](img/pipelines-goal.svg) ## Builds From 86558d53ddcf008fff396d25c38c9462fb77f615 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 10 Feb 2017 19:41:15 +0000 Subject: [PATCH 370/488] Adds verification in case the endpoint already has `.json` --- .../commit/pipelines/pipelines_service.js.es6 | 19 +++++++++++++++++-- .../merge_requests/create_new_mr_spec.rb | 3 +++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 index e9cae893857..8ae98f9bf97 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 @@ -8,8 +8,23 @@ * Uses Vue.Resource */ class PipelinesService { - constructor(endpoint) { - this.pipelines = Vue.resource(`${endpoint}.json`); + + /** + * FIXME: The url provided to request the pipelines in the new merge request + * page already has `.json`. + * This should be fixed when the endpoint is improved. + * + * @param {String} root + */ + constructor(root) { + let endpoint; + + if (root.indexOf('.json') === -1) { + endpoint = `${root}.json`; + } else { + endpoint = root; + } + this.pipelines = Vue.resource(endpoint); } /** diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index e853fb7e016..0832a3656a8 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Create New Merge Request', feature: true, js: true do + include WaitForVueResource + let(:user) { create(:user) } let(:project) { create(:project, :public) } @@ -99,6 +101,7 @@ feature 'Create New Merge Request', feature: true, js: true do page.within('.merge-request') do click_link 'Pipelines' + wait_for_vue_resource expect(page).to have_content "##{pipeline.id}" end From 7b20784715ea4f32fd8d83d1ab9403056069e9f7 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 10 Feb 2017 15:21:45 -0500 Subject: [PATCH 371/488] Revert "Merge branch 'add-additional-checks-to-ca-data' into 'master' " This reverts commit b7c5ca499d9c26494736d92505116bbb294c63d6, reversing changes made to 9745c98bb77a1a1ddd964f58cc4d058a665eb9ee. --- .../cycle_analytics_bundle.js.es6 | 4 +- .../cycle_analytics_store.js.es6 | 47 ++++----- .../default_event_objects.js.es6 | 98 ------------------- .../javascripts/lib/utils/text_utility.js | 10 +- app/assets/javascripts/wikis.js.es6 | 6 +- app/serializers/analytics_stage_entity.rb | 1 - .../projects/cycle_analytics/show.html.haml | 2 +- lib/gitlab/cycle_analytics/code_stage.rb | 4 - lib/gitlab/cycle_analytics/issue_stage.rb | 4 - lib/gitlab/cycle_analytics/plan_stage.rb | 4 - .../cycle_analytics/production_stage.rb | 4 - lib/gitlab/cycle_analytics/review_stage.rb | 4 - lib/gitlab/cycle_analytics/staging_stage.rb | 4 - lib/gitlab/cycle_analytics/test_stage.rb | 4 - 14 files changed, 29 insertions(+), 167 deletions(-) delete mode 100644 app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 index f161eb23795..c41c57c1dcd 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 @@ -97,7 +97,7 @@ $(() => { } this.isLoadingStage = true; - cycleAnalyticsStore.setStageEvents([], stage); + cycleAnalyticsStore.setStageEvents([]); cycleAnalyticsStore.setActiveStage(stage); cycleAnalyticsService @@ -107,7 +107,7 @@ $(() => { }) .done((response) => { this.isEmptyStage = !response.events.length; - cycleAnalyticsStore.setStageEvents(response.events, stage); + cycleAnalyticsStore.setStageEvents(response.events); }) .error(() => { this.isEmptyStage = true; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 index 3efeb141008..be732971c7f 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 @@ -1,8 +1,4 @@ /* eslint-disable no-param-reassign */ - -require('../lib/utils/text_utility'); -const DEFAULT_EVENT_OBJECTS = require('./default_event_objects'); - ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; @@ -38,12 +34,11 @@ const DEFAULT_EVENT_OBJECTS = require('./default_event_objects'); }); newData.stages.forEach((item) => { - const stageSlug = gl.text.dasherize(item.title.toLowerCase()); + const stageName = item.title.toLowerCase(); item.active = false; - item.isUserAllowed = data.permissions[stageSlug]; - item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug]; - item.component = `stage-${stageSlug}-component`; - item.slug = stageSlug; + item.isUserAllowed = data.permissions[stageName]; + item.emptyStageText = EMPTY_STAGE_TEXTS[stageName]; + item.component = `stage-${stageName}-component`; }); newData.analytics = data; return newData; @@ -63,33 +58,31 @@ const DEFAULT_EVENT_OBJECTS = require('./default_event_objects'); this.deactivateAllStages(); stage.active = true; }, - setStageEvents(events, stage) { - this.state.events = this.decorateEvents(events, stage); + setStageEvents(events) { + this.state.events = this.decorateEvents(events); }, - decorateEvents(events, stage) { + decorateEvents(events) { const newEvents = []; events.forEach((item) => { if (!item) return; - const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item); + item.totalTime = item.total_time; + item.author.webUrl = item.author.web_url; + item.author.avatarUrl = item.author.avatar_url; - eventItem.totalTime = eventItem.total_time; - eventItem.author.webUrl = eventItem.author.web_url; - eventItem.author.avatarUrl = eventItem.author.avatar_url; + if (item.created_at) item.createdAt = item.created_at; + if (item.short_sha) item.shortSha = item.short_sha; + if (item.commit_url) item.commitUrl = item.commit_url; - if (eventItem.created_at) eventItem.createdAt = eventItem.created_at; - if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha; - if (eventItem.commit_url) eventItem.commitUrl = eventItem.commit_url; + delete item.author.web_url; + delete item.author.avatar_url; + delete item.total_time; + delete item.created_at; + delete item.short_sha; + delete item.commit_url; - delete eventItem.author.web_url; - delete eventItem.author.avatar_url; - delete eventItem.total_time; - delete eventItem.created_at; - delete eventItem.short_sha; - delete eventItem.commit_url; - - newEvents.push(eventItem); + newEvents.push(item); }); return newEvents; diff --git a/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 b/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 deleted file mode 100644 index cfaf9835bf8..00000000000 --- a/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 +++ /dev/null @@ -1,98 +0,0 @@ -module.exports = { - issue: { - created_at: '', - url: '', - iid: '', - title: '', - total_time: {}, - author: { - avatar_url: '', - id: '', - name: '', - web_url: '', - }, - }, - plan: { - title: '', - commit_url: '', - short_sha: '', - total_time: {}, - author: { - name: '', - id: '', - avatar_url: '', - web_url: '', - }, - }, - code: { - title: '', - iid: '', - created_at: '', - url: '', - total_time: {}, - author: { - name: '', - id: '', - avatar_url: '', - web_url: '', - }, - }, - test: { - name: '', - id: '', - date: '', - url: '', - short_sha: '', - commit_url: '', - total_time: {}, - branch: { - name: '', - url: '', - }, - }, - review: { - title: '', - iid: '', - created_at: '', - url: '', - state: '', - total_time: {}, - author: { - name: '', - id: '', - avatar_url: '', - web_url: '', - }, - }, - staging: { - id: '', - short_sha: '', - date: '', - url: '', - commit_url: '', - total_time: {}, - author: { - name: '', - id: '', - avatar_url: '', - web_url: '', - }, - branch: { - name: '', - url: '', - }, - }, - production: { - title: '', - created_at: '', - url: '', - iid: '', - total_time: {}, - author: { - name: '', - id: '', - avatar_url: '', - web_url: '', - }, - }, -}; diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 326b7cb7f57..d9370db0cf2 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -1,7 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len */ -require('vendor/latinise'); - (function() { (function(w) { var base; @@ -166,14 +164,8 @@ require('vendor/latinise'); gl.text.pluralize = function(str, count) { return str + (count > 1 || count === 0 ? 's' : ''); }; - gl.text.truncate = function(string, maxLength) { + return gl.text.truncate = function(string, maxLength) { return string.substr(0, (maxLength - 3)) + '...'; }; - gl.text.dasherize = function(str) { - return str.replace(/[_\s]+/g, '-'); - }; - gl.text.slugify = function(str) { - return str.trim().toLowerCase().latinise(); - }; })(window); }).call(this); diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6 index 75fd1394a03..ef99b2e92f0 100644 --- a/app/assets/javascripts/wikis.js.es6 +++ b/app/assets/javascripts/wikis.js.es6 @@ -1,10 +1,14 @@ /* eslint-disable no-param-reassign */ /* global Breakpoints */ +require('vendor/latinise'); require('./breakpoints'); require('vendor/jquery.nicescroll'); ((global) => { + const dasherize = str => str.replace(/[_\s]+/g, '-'); + const slugify = str => dasherize(str.trim().toLowerCase().latinise()); + class Wikis { constructor() { this.bp = Breakpoints.get(); @@ -30,7 +34,7 @@ require('vendor/jquery.nicescroll'); if (!this.newWikiForm) return; const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); - const slug = gl.text.slugify(slugInput.value); + const slug = slugify(slugInput.value); if (slug.length > 0) { const wikisPath = slugInput.getAttribute('data-wikis-path'); diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb index 69bf693de8d..a559d0850c4 100644 --- a/app/serializers/analytics_stage_entity.rb +++ b/app/serializers/analytics_stage_entity.rb @@ -2,7 +2,6 @@ class AnalyticsStageEntity < Grape::Entity include EntityDateHelper expose :title - expose :legend expose :description expose :median, as: :value do |stage| diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index ad904a8708e..5405ff16bea 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -44,7 +44,7 @@ Last 90 days .stage-panel-container .panel.panel-default.stage-panel - .panel-heading + .panel-heading %nav.col-headers %ul %li.stage-header diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb index 1e52b6614a1..d1bc2055ba8 100644 --- a/lib/gitlab/cycle_analytics/code_stage.rb +++ b/lib/gitlab/cycle_analytics/code_stage.rb @@ -13,10 +13,6 @@ module Gitlab :code end - def legend - "Related Merge Requests" - end - def description "Time until first merge request" end diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb index 213994988a5..d2068fbc38f 100644 --- a/lib/gitlab/cycle_analytics/issue_stage.rb +++ b/lib/gitlab/cycle_analytics/issue_stage.rb @@ -14,10 +14,6 @@ module Gitlab :issue end - def legend - "Related Issues" - end - def description "Time before an issue gets scheduled" end diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb index 45d51d30ccc..3b4dfc6a30e 100644 --- a/lib/gitlab/cycle_analytics/plan_stage.rb +++ b/lib/gitlab/cycle_analytics/plan_stage.rb @@ -14,10 +14,6 @@ module Gitlab :plan end - def legend - "Related Commits" - end - def description "Time before an issue starts implementation" end diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb index 9f387a02945..2a6bcc80116 100644 --- a/lib/gitlab/cycle_analytics/production_stage.rb +++ b/lib/gitlab/cycle_analytics/production_stage.rb @@ -15,10 +15,6 @@ module Gitlab :production end - def legend - "Related Issues" - end - def description "From issue creation until deploy to production" end diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb index 4744be834de..fbaa3010d81 100644 --- a/lib/gitlab/cycle_analytics/review_stage.rb +++ b/lib/gitlab/cycle_analytics/review_stage.rb @@ -13,10 +13,6 @@ module Gitlab :review end - def legend - "Relative Merged Requests" - end - def description "Time between merge request creation and merge/close" end diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb index 3cdbe04fbaf..945909a4d62 100644 --- a/lib/gitlab/cycle_analytics/staging_stage.rb +++ b/lib/gitlab/cycle_analytics/staging_stage.rb @@ -14,10 +14,6 @@ module Gitlab :staging end - def legend - "Relative Deployed Builds" - end - def description "From merge request merge until deploy to production" end diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb index e96943833bc..0079d56e0e4 100644 --- a/lib/gitlab/cycle_analytics/test_stage.rb +++ b/lib/gitlab/cycle_analytics/test_stage.rb @@ -13,10 +13,6 @@ module Gitlab :test end - def legend - "Relative Builds Trigger by Commits" - end - def description "Total test time for all commits/merges" end From a97dcc077c68f4f320cd7a5686b9056adfef6c09 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 8 Feb 2017 18:15:47 +0100 Subject: [PATCH 372/488] Add method for creating foreign keys concurrently This method allows one to create foreign keys without blocking access to the source table, but only on PostgreSQL. When creating a regular foreign key the "ALTER TABLE" statement used for this won't return until all data has been validated. This statement in turn will acquire a lock on the source table. As a result this lock can be held for quite a long amount of time, depending on the number of rows and system load. By breaking up the foreign key creation process in two steps (creation, and validation) we can reduce the amount of locking to a minimum. Locking is still necessary for the "ALTER TABLE" statement that adds the constraint, but this is a fast process and so will only block access for a few milliseconds. --- lib/gitlab/database/migration_helpers.rb | 50 +++++++++++- .../gitlab/database/migration_helpers_spec.rb | 76 +++++++++++++++++-- 2 files changed, 119 insertions(+), 7 deletions(-) diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 0bd6e148ba8..4800a509b37 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -26,11 +26,59 @@ module Gitlab add_index(table_name, column_name, options) end + # Adds a foreign key with only minimal locking on the tables involved. + # + # This method only requires minimal locking when using PostgreSQL. When + # using MySQL this method will use Rails' default `add_foreign_key`. + # + # source - The source table containing the foreign key. + # target - The target table the key points to. + # column - The name of the column to create the foreign key on. + # on_delete - The action to perform when associated data is removed, + # defaults to "CASCADE". + def add_concurrent_foreign_key(source, target, column:, on_delete: :cascade) + # Transactions would result in ALTER TABLE locks being held for the + # duration of the transaction, defeating the purpose of this method. + if transaction_open? + raise 'add_concurrent_foreign_key can not be run inside a transaction' + end + + # While MySQL does allow disabling of foreign keys it has no equivalent + # of PostgreSQL's "VALIDATE CONSTRAINT". As a result we'll just fall + # back to the normal foreign key procedure. + if Database.mysql? + return add_foreign_key(source, target, + column: column, + on_delete: on_delete) + end + + disable_statement_timeout + + key_name = "fk_#{source}_#{target}_#{column}" + + # Using NOT VALID allows us to create a key without immediately + # validating it. This means we keep the ALTER TABLE lock only for a + # short period of time. The key _is_ enforced for any newly created + # data. + execute <<-EOF.strip_heredoc + ALTER TABLE #{source} + ADD CONSTRAINT #{key_name} + FOREIGN KEY (#{column}) + REFERENCES #{target} (id) + ON DELETE #{on_delete} NOT VALID; + EOF + + # Validate the existing constraint. This can potentially take a very + # long time to complete, but fortunately does not lock the source table + # while running. + execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{key_name};") + end + # Long-running migrations may take more than the timeout allowed by # the database. Disable the session's statement timeout to ensure # migrations don't get killed prematurely. (PostgreSQL only) def disable_statement_timeout - ActiveRecord::Base.connection.execute('SET statement_timeout TO 0') if Database.postgresql? + execute('SET statement_timeout TO 0') if Database.postgresql? end # Updates the value of a column in batches. diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 7fd25b9e5bf..e94ca4fcfd2 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -12,15 +12,14 @@ describe Gitlab::Database::MigrationHelpers, lib: true do describe '#add_concurrent_index' do context 'outside a transaction' do before do - expect(model).to receive(:transaction_open?).and_return(false) - - unless Gitlab::Database.postgresql? - allow_any_instance_of(Gitlab::Database::MigrationHelpers).to receive(:disable_statement_timeout) - end + allow(model).to receive(:transaction_open?).and_return(false) end context 'using PostgreSQL' do - before { expect(Gitlab::Database).to receive(:postgresql?).and_return(true) } + before do + allow(Gitlab::Database).to receive(:postgresql?).and_return(true) + allow(model).to receive(:disable_statement_timeout) + end it 'creates the index concurrently' do expect(model).to receive(:add_index). @@ -59,6 +58,71 @@ describe Gitlab::Database::MigrationHelpers, lib: true do end end + describe '#add_concurrent_foreign_key' do + context 'inside a transaction' do + it 'raises an error' do + expect(model).to receive(:transaction_open?).and_return(true) + + expect do + model.add_concurrent_foreign_key(:projects, :users, column: :user_id) + end.to raise_error(RuntimeError) + end + end + + context 'outside a transaction' do + before do + allow(model).to receive(:transaction_open?).and_return(false) + end + + context 'using MySQL' do + it 'creates a regular foreign key' do + allow(Gitlab::Database).to receive(:mysql?).and_return(true) + + expect(model).to receive(:add_foreign_key). + with(:projects, :users, column: :user_id, on_delete: :cascade) + + model.add_concurrent_foreign_key(:projects, :users, column: :user_id) + end + end + + context 'using PostgreSQL' do + before do + allow(Gitlab::Database).to receive(:mysql?).and_return(false) + end + + it 'creates a concurrent foreign key' do + expect(model).to receive(:disable_statement_timeout) + expect(model).to receive(:execute).ordered.with(/NOT VALID/) + expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/) + + model.add_concurrent_foreign_key(:projects, :users, column: :user_id) + end + end + end + end + + describe '#disable_statement_timeout' do + context 'using PostgreSQL' do + it 'disables statement timeouts' do + expect(Gitlab::Database).to receive(:postgresql?).and_return(true) + + expect(model).to receive(:execute).with('SET statement_timeout TO 0') + + model.disable_statement_timeout + end + end + + context 'using MySQL' do + it 'does nothing' do + expect(Gitlab::Database).to receive(:postgresql?).and_return(false) + + expect(model).not_to receive(:execute) + + model.disable_statement_timeout + end + end + end + describe '#update_column_in_batches' do before do create_list(:empty_project, 5) From 766060bcdf3ff7c37f471b58e5d13721b263e37b Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 9 Feb 2017 16:52:43 +0100 Subject: [PATCH 373/488] Enforce use of add_concurrent_foreign_key This adds a Rubocop rule to enforce the use of add_concurrent_foreign_key instead of the regular add_foreign_key method. This cop has been disabled for existing migrations so we don't need to change those. --- .../20160919145149_add_group_id_to_labels.rb | 2 +- ...dd_pipeline_id_to_merge_request_metrics.rb | 2 +- ...1171301_add_project_id_to_subscriptions.rb | 2 +- .../migration/add_concurrent_foreign_key.rb | 27 +++++++++++++++ rubocop/rubocop.rb | 1 + .../add_concurrent_foreign_key_spec.rb | 33 +++++++++++++++++++ 6 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 rubocop/cop/migration/add_concurrent_foreign_key.rb create mode 100644 spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb diff --git a/db/migrate/20160919145149_add_group_id_to_labels.rb b/db/migrate/20160919145149_add_group_id_to_labels.rb index 05e21af0584..d10f3a6d104 100644 --- a/db/migrate/20160919145149_add_group_id_to_labels.rb +++ b/db/migrate/20160919145149_add_group_id_to_labels.rb @@ -7,7 +7,7 @@ class AddGroupIdToLabels < ActiveRecord::Migration def change add_column :labels, :group_id, :integer - add_foreign_key :labels, :namespaces, column: :group_id, on_delete: :cascade + add_foreign_key :labels, :namespaces, column: :group_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey add_concurrent_index :labels, :group_id end end diff --git a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb index f49df6802a7..2abfe47b776 100644 --- a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb +++ b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb @@ -28,6 +28,6 @@ class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration def change add_column :merge_request_metrics, :pipeline_id, :integer add_concurrent_index :merge_request_metrics, :pipeline_id - add_foreign_key :merge_request_metrics, :ci_commits, column: :pipeline_id, on_delete: :cascade + add_foreign_key :merge_request_metrics, :ci_commits, column: :pipeline_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey end end diff --git a/db/migrate/20161031171301_add_project_id_to_subscriptions.rb b/db/migrate/20161031171301_add_project_id_to_subscriptions.rb index 97534679b59..d5c343dc527 100644 --- a/db/migrate/20161031171301_add_project_id_to_subscriptions.rb +++ b/db/migrate/20161031171301_add_project_id_to_subscriptions.rb @@ -5,7 +5,7 @@ class AddProjectIdToSubscriptions < ActiveRecord::Migration def up add_column :subscriptions, :project_id, :integer - add_foreign_key :subscriptions, :projects, column: :project_id, on_delete: :cascade + add_foreign_key :subscriptions, :projects, column: :project_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey end def down diff --git a/rubocop/cop/migration/add_concurrent_foreign_key.rb b/rubocop/cop/migration/add_concurrent_foreign_key.rb new file mode 100644 index 00000000000..e40a7087a47 --- /dev/null +++ b/rubocop/cop/migration/add_concurrent_foreign_key.rb @@ -0,0 +1,27 @@ +require_relative '../../migration_helpers' + +module RuboCop + module Cop + module Migration + # Cop that checks if `add_concurrent_foreign_key` is used instead of + # `add_foreign_key`. + class AddConcurrentForeignKey < RuboCop::Cop::Cop + include MigrationHelpers + + MSG = '`add_foreign_key` requires downtime, use `add_concurrent_foreign_key` instead' + + def on_send(node) + return unless in_migration?(node) + + name = node.children[1] + + add_offense(node, :selector) if name == :add_foreign_key + end + + def method_name(node) + node.children.first + end + end + end + end +end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index aa35fb1701c..d4266d0deae 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -1,4 +1,5 @@ require_relative 'cop/gem_fetcher' require_relative 'cop/migration/add_column' require_relative 'cop/migration/add_column_with_default' +require_relative 'cop/migration/add_concurrent_foreign_key' require_relative 'cop/migration/add_index' diff --git a/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb new file mode 100644 index 00000000000..7cb24dc5646 --- /dev/null +++ b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../../rubocop/cop/migration/add_concurrent_foreign_key' + +describe RuboCop::Cop::Migration::AddConcurrentForeignKey do + include CopHelper + + let(:cop) { described_class.new } + + context 'outside of a migration' do + it 'does not register any offenses' do + inspect_source(cop, 'def up; add_foreign_key(:projects, :users, column: :user_id); end') + + expect(cop.offenses).to be_empty + end + end + + context 'in a migration' do + before do + allow(cop).to receive(:in_migration?).and_return(true) + end + + it 'registers an offense when using add_foreign_key' do + inspect_source(cop, 'def up; add_foreign_key(:projects, :users, column: :user_id); end') + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + end + end + end +end From 060921921d920e9f4942885507a7296424ddf08c Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 17 Nov 2016 16:46:48 -0700 Subject: [PATCH 374/488] Remove fixed positioning from navbar --- app/assets/stylesheets/framework/nav.scss | 4 ---- app/assets/stylesheets/framework/sidebar.scss | 2 +- app/views/layouts/header/_default.html.haml | 2 +- spec/javascripts/fixtures/header.html.haml | 2 +- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index fd081c2d7e1..d62f34ccb85 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -283,10 +283,7 @@ } .layout-nav { - position: fixed; - top: $header-height; width: 100%; - z-index: 11; background: $gray-light; border-bottom: 1px solid $border-color; transition: padding $sidebar-transition-duration; @@ -419,7 +416,6 @@ } .page-with-layout-nav { - margin-top: $header-height + 2; .right-sidebar { top: ($header-height * 2) + 2; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index f0b03710c79..41e50ce7bb9 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -1,5 +1,5 @@ .page-with-sidebar { - padding: $header-height 0 25px; + padding-bottom: 25px; transition: padding $sidebar-transition-duration; &.page-sidebar-pinned { diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 9ecc0d11c95..59082ce5fd5 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -1,4 +1,4 @@ -%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } +%header.navbar.navbar-gitlab{ class: nav_header_class } %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content .container-fluid .header-content diff --git a/spec/javascripts/fixtures/header.html.haml b/spec/javascripts/fixtures/header.html.haml index 4db2ef604de..f397f69e753 100644 --- a/spec/javascripts/fixtures/header.html.haml +++ b/spec/javascripts/fixtures/header.html.haml @@ -1,4 +1,4 @@ -%header.navbar.navbar-fixed-top.navbar-gitlab.nav_header_class +%header.navbar.navbar-gitlab.nav_header_class .container-fluid .header-content %button.side-nav-toggle From eefbc2bf738b9b715aad3777868d4dbeaadf612e Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Fri, 18 Nov 2016 13:45:51 -0700 Subject: [PATCH 375/488] Fix sidebar scrolling --- app/assets/javascripts/build.js | 6 ++++++ app/assets/stylesheets/framework/sidebar.scss | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 0152be88b48..a378e5e27b8 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -68,9 +68,15 @@ Build.prototype.initSidebar = function() { this.$sidebar = $('.js-build-sidebar'); this.sidebarTranslationLimits = { +<<<<<<< 3ee255139ab555ec49a177d3b2eed65580f36c4f min: $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() }; this.sidebarTranslationLimits.max = this.sidebarTranslationLimits.min + $('.scrolling-tabs-container').outerHeight(); +======= + min: 0 + } + this.sidebarTranslationLimits.max = $('.scrolling-tabs-container').outerHeight() + $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); +>>>>>>> Fix sidebar scrolling this.$sidebar.css({ top: this.sidebarTranslationLimits.max }); diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 41e50ce7bb9..d60661222e9 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -208,7 +208,9 @@ header.header-sidebar-pinned { padding-right: 0; @media (min-width: $screen-sm-min) { - padding-right: $sidebar_collapsed_width; + .content-wrapper { + padding-right: $sidebar_collapsed_width; + } .merge-request-tabs-holder.affix { right: $sidebar_collapsed_width; @@ -234,7 +236,9 @@ header.header-sidebar-pinned { } @media (min-width: $screen-md-min) { - padding-right: $gutter_width; + .content-wrapper { + padding-right: $gutter_width; + } &:not(.with-overlay) .merge-request-tabs-holder.affix { right: $gutter_width; From a11e798aa0fdc2f67bdc0fc5ad611a6872af0e9f Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Wed, 4 Jan 2017 12:21:28 -0600 Subject: [PATCH 376/488] Keep sidebars absolute until fixed at top; remove unneeded JS --- app/assets/javascripts/build.js | 12 +++--------- app/assets/javascripts/merge_request_tabs.js.es6 | 2 +- app/assets/stylesheets/framework/nav.scss | 10 ++++++++-- app/assets/stylesheets/framework/sidebar.scss | 5 +++++ app/assets/stylesheets/pages/issuable.scss | 3 ++- app/assets/stylesheets/pages/merge_requests.scss | 2 +- app/assets/stylesheets/pages/tree.scss | 2 -- app/views/projects/builds/_sidebar.html.haml | 2 +- 8 files changed, 21 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index a378e5e27b8..eed447d54a0 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -67,6 +67,7 @@ Build.prototype.initSidebar = function() { this.$sidebar = $('.js-build-sidebar'); +<<<<<<< 36beffc12461d2e479ad8b000b7ba5b6ea40cd33 this.sidebarTranslationLimits = { <<<<<<< 3ee255139ab555ec49a177d3b2eed65580f36c4f min: $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() @@ -80,9 +81,10 @@ this.$sidebar.css({ top: this.sidebarTranslationLimits.max }); +======= +>>>>>>> Keep sidebars absolute until fixed at top; remove unneeded JS this.$sidebar.niceScroll(); this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar); - this.$document.off('scroll.translateSidebar').on('scroll.translateSidebar', this.translateSidebar.bind(this)); }; Build.prototype.location = function() { @@ -237,14 +239,6 @@ return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; }; - Build.prototype.translateSidebar = function(e) { - var newPosition = this.sidebarTranslationLimits.max - (document.body.scrollTop || document.documentElement.scrollTop); - if (newPosition < this.sidebarTranslationLimits.min) newPosition = this.sidebarTranslationLimits.min; - this.$sidebar.css({ - top: newPosition - }); - }; - Build.prototype.toggleSidebar = function(shouldHide) { var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined; this.$buildScroll.toggleClass('sidebar-expanded', shouldShow) diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index af1ba9ecaf3..e74658d59bd 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -337,7 +337,7 @@ require('./flash'); .affix({ offset: { top: () => ( - $diffTabs.offset().top - $tabs.height() - $fixedNav.height() - $layoutNav.height() + $diffTabs.offset().top - $tabs.height() ), }, }) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index d62f34ccb85..674d3bb45aa 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -416,14 +416,20 @@ } .page-with-layout-nav { - .right-sidebar { top: ($header-height * 2) + 2; } + + .build-sidebar { + top: ($header-height * 3) + 3; + + &.affix { + top: 0; + } + } } .activities { - .nav-block { border-bottom: 1px solid $border-color; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index d60661222e9..20bcb1eeb23 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -256,4 +256,9 @@ header.header-sidebar-pinned { .right-sidebar { border-left: 1px solid $border-color; + + &.affix { + position: fixed; + top: 0; + } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 1a53730bed5..130103a2702 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -189,7 +189,8 @@ } .right-sidebar { - position: fixed; + position: absolute; + height: 100%; top: $header-height; bottom: 0; right: 0; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 1431673027f..c02a65b0903 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -479,7 +479,7 @@ background-color: $white-light; &.affix { - top: 100px; + top: 0; left: 0; z-index: 10; transition: right .15s; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 948921efc0b..8fafe472621 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -171,8 +171,6 @@ .tree-controls { float: right; margin-top: 11px; - position: relative; - z-index: 2; .project-action-button { margin-left: $btn-side-margin; diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index 56fc5f5e68b..f4a49ba71c6 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -1,6 +1,6 @@ - builds = @build.pipeline.builds.to_a -%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar +%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar{ data: { "offset-top" => "151", "spy" => "affix" } } .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default Job %strong ##{@build.id} From 19cb7b0a3ddbcc94f5c46a60d1494d53bd4faaee Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Wed, 4 Jan 2017 13:27:20 -0600 Subject: [PATCH 377/488] Remove navbar height offsets --- app/assets/javascripts/build.js | 2 +- .../components/jump_to_discussion.js.es6 | 2 +- .../javascripts/lib/utils/common_utils.js.es6 | 24 ++----------------- .../javascripts/merge_request_tabs.js.es6 | 7 +----- 4 files changed, 5 insertions(+), 30 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index eed447d54a0..86fb08c00a5 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -285,7 +285,7 @@ e.preventDefault(); $currentTarget = $(e.currentTarget); $.scrollTo($currentTarget.attr('href'), { - offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()) + offset: 0 }); }; diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 index 57cb0d0ae6e..283dc330cad 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 @@ -181,7 +181,7 @@ const Vue = require('vue'); } $.scrollTo($target, { - offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()) + offset: 0 }); } }, diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index 5becf688652..0966adcfb68 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -69,30 +69,18 @@ var hash = w.gl.utils.getLocationHash(); if (!hash) return; - // This is required to handle non-unicode characters in hash - hash = decodeURIComponent(hash); - - var navbar = document.querySelector('.navbar-gitlab'); - var subnav = document.querySelector('.layout-nav'); - var fixedTabs = document.querySelector('.js-tabs-affix'); - - var adjustment = 0; - if (navbar) adjustment -= navbar.offsetHeight; - if (subnav) adjustment -= subnav.offsetHeight; - // scroll to user-generated markdown anchor if we cannot find a match if (document.getElementById(hash) === null) { var target = document.getElementById('user-content-' + hash); if (target && target.scrollIntoView) { target.scrollIntoView(true); - window.scrollBy(0, adjustment); } } else { // only adjust for fixedTabs when not targeting user-generated content + var fixedTabs = document.querySelector('.js-tabs-affix'); if (fixedTabs) { - adjustment -= fixedTabs.offsetHeight; + window.scrollBy(0, -fixedTabs.offsetHeight); } - window.scrollBy(0, adjustment); } }; @@ -137,14 +125,6 @@ return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; }; - gl.utils.isMetaClick = function(e) { - // Identify following special clicks - // 1) Cmd + Click on Mac (e.metaKey) - // 2) Ctrl + Click on PC (e.ctrlKey) - // 3) Middle-click or Mouse Wheel Click (e.which is 2) - return e.metaKey || e.ctrlKey || e.which === 2; - }; - gl.utils.scrollToElement = function($el) { var top = $el.offset().top; gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height(); diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index e74658d59bd..fad96ff2882 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -125,9 +125,8 @@ require('./flash'); if (this.diffViewType() === 'parallel') { this.expandViewContainer(); } - const navBarHeight = $('.navbar-gitlab').outerHeight(); $.scrollTo('.merge-request-details .merge-request-tabs', { - offset: -navBarHeight, + offset: 0, }); } else { this.expandView(); @@ -141,8 +140,6 @@ require('./flash'); scrollToElement(container) { if (location.hash) { const offset = 0 - ( - $('.navbar-gitlab').outerHeight() + - $('.layout-nav').outerHeight() + $('.js-tabs-affix').outerHeight() ); const $el = $(`${container} ${location.hash}:not(.match)`); @@ -330,8 +327,6 @@ require('./flash'); if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; const $diffTabs = $('#diff-notes-app'); - const $fixedNav = $('.navbar-fixed-top'); - const $layoutNav = $('.layout-nav'); $tabs.off('affix.bs.affix affix-top.bs.affix') .affix({ From 59e5748a1ff4fb63aad7d9bcf578678eac824cb0 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Wed, 4 Jan 2017 15:31:08 -0600 Subject: [PATCH 378/488] Fix pinned sidebar alignment --- app/assets/javascripts/sidebar.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6 index cbb2ae9f1bd..24423dccf1f 100644 --- a/app/assets/javascripts/sidebar.js.es6 +++ b/app/assets/javascripts/sidebar.js.es6 @@ -6,7 +6,7 @@ const sidebarBreakpoint = 1024; const pageSelector = '.page-with-sidebar'; - const navbarSelector = '.navbar-fixed-top'; + const navbarSelector = '.navbar-gitlab'; const sidebarWrapperSelector = '.sidebar-wrapper'; const sidebarContentSelector = '.nav-sidebar'; From 8891536c47e0d1619df8527ee5c15fb2c5d020d4 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Wed, 18 Jan 2017 15:25:35 -0600 Subject: [PATCH 379/488] Refactor JS --- app/assets/javascripts/merge_request_tabs.js.es6 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index fad96ff2882..7540889bce6 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -139,9 +139,7 @@ require('./flash'); scrollToElement(container) { if (location.hash) { - const offset = 0 - ( - $('.js-tabs-affix').outerHeight() - ); + const offset = -$('.js-tabs-affix').outerHeight(); const $el = $(`${container} ${location.hash}:not(.match)`); if ($el.length) { $.scrollTo($el[0], { offset }); From bc6ad4f0fdbfedefcb7f439da4ca5ee5f0a4a416 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Fri, 20 Jan 2017 14:44:06 -0600 Subject: [PATCH 380/488] Fix build sidebar scrolling --- app/assets/javascripts/build.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 86fb08c00a5..c5a962dd199 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -67,22 +67,6 @@ Build.prototype.initSidebar = function() { this.$sidebar = $('.js-build-sidebar'); -<<<<<<< 36beffc12461d2e479ad8b000b7ba5b6ea40cd33 - this.sidebarTranslationLimits = { -<<<<<<< 3ee255139ab555ec49a177d3b2eed65580f36c4f - min: $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() - }; - this.sidebarTranslationLimits.max = this.sidebarTranslationLimits.min + $('.scrolling-tabs-container').outerHeight(); -======= - min: 0 - } - this.sidebarTranslationLimits.max = $('.scrolling-tabs-container').outerHeight() + $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); ->>>>>>> Fix sidebar scrolling - this.$sidebar.css({ - top: this.sidebarTranslationLimits.max - }); -======= ->>>>>>> Keep sidebars absolute until fixed at top; remove unneeded JS this.$sidebar.niceScroll(); this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar); }; From 465316ec22ba6083b5a6bf7dab21518564604c56 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Tue, 24 Jan 2017 12:50:22 -0600 Subject: [PATCH 381/488] Fix failing conflicts test --- spec/features/merge_requests/conflicts_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index 5bc4ab2dfe5..d710a780111 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -141,7 +141,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do click_on 'Changes' wait_for_ajax - find('.click-to-expand').click + click_link 'Expand all' wait_for_ajax expect(page).to have_content('Gregor Samsa woke from troubled dreams') From 0544c4ed7690abfd4d20d375ace09cc28cf4bf18 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 26 Jan 2017 08:57:42 -0600 Subject: [PATCH 382/488] Fix issue boards sidebar alignment and sidebar toggle spec --- app/assets/stylesheets/pages/boards.scss | 6 +----- spec/features/boards/sidebar_spec.rb | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index b362cc758cc..9a36d76136b 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -298,12 +298,8 @@ .issue-boards-sidebar { &.right-sidebar { - top: 153px; + top: 0; bottom: 0; - - @media (min-width: $screen-sm-min) { - top: 220px; - } } .issuable-sidebar-header { diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index bad6b56a18a..7651364703e 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -54,7 +54,7 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_selector('.issue-boards-sidebar') - find('.gutter-toggle').click + find('.gutter-toggle').trigger('click') expect(page).not_to have_selector('.issue-boards-sidebar') end From fe215dacb823d022096264e2dfa31f11d0b1e670 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 26 Jan 2017 09:23:57 -0600 Subject: [PATCH 383/488] Fix time tracking spec --- spec/support/time_tracking_shared_examples.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/time_tracking_shared_examples.rb index 02657684b57..52f4fabdc47 100644 --- a/spec/support/time_tracking_shared_examples.rb +++ b/spec/support/time_tracking_shared_examples.rb @@ -77,6 +77,6 @@ end def submit_time(slash_command) fill_in 'note[note]', with: slash_command - click_button 'Comment' + find('.comment-btn').trigger('click') wait_for_ajax end From 40d4d6d69a3d811d13e9c5c2c2437eeda713ad1f Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 26 Jan 2017 11:06:17 -0600 Subject: [PATCH 384/488] Fix comment button test for slash commands --- spec/support/slash_commands_helpers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/support/slash_commands_helpers.rb b/spec/support/slash_commands_helpers.rb index df483afa0e3..0d91fe5fd5d 100644 --- a/spec/support/slash_commands_helpers.rb +++ b/spec/support/slash_commands_helpers.rb @@ -3,7 +3,7 @@ module SlashCommandsHelpers Sidekiq::Testing.fake! do page.within('.js-main-target-form') do fill_in 'note[note]', with: text - click_button 'Comment' + find('.comment-btn').trigger('click') end end end From 647d2b08cde74228728aeb12486c23e5f8e1b317 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Fri, 3 Feb 2017 15:22:04 -0600 Subject: [PATCH 385/488] Add sticky sidebar on wiki page --- app/views/projects/wikis/_sidebar.html.haml | 2 +- app/views/shared/issuable/_sidebar.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index cad9c15a49e..456477824e5 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -1,4 +1,4 @@ -%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar +%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } } .block.wiki-sidebar-header.append-bottom-default %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" } = icon('angle-double-right') diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 77fc44fa5cc..1ccb09c36f5 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -2,7 +2,7 @@ - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('issuable') -%aside.right-sidebar{ class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } +%aside.right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } .issuable-sidebar - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .block.issuable-sidebar-header From 178be67df221d46c5053a72917860775b343bc7d Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Mon, 6 Feb 2017 12:11:34 -0600 Subject: [PATCH 386/488] Set height of fixed sidebars with js --- app/assets/javascripts/sidebar.js.es6 | 11 +++++++++++ app/assets/stylesheets/pages/issuable.scss | 1 - 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6 index 24423dccf1f..6c3a1f3307a 100644 --- a/app/assets/javascripts/sidebar.js.es6 +++ b/app/assets/javascripts/sidebar.js.es6 @@ -35,13 +35,16 @@ window.innerWidth >= sidebarBreakpoint && $(pageSelector).hasClass(expandedPageClass) ); + $(window).on('resize', () => this.setSidebarHeight()); $(document) .on('click', sidebarToggleSelector, () => this.toggleSidebar()) .on('click', pinnedToggleSelector, () => this.togglePinnedState()) .on('click', 'html, body, a, button', (e) => this.handleClickEvent(e)) .on('DOMContentLoaded', () => this.renderState()) + .on('scroll', () => this.setSidebarHeight()) .on('todo:toggle', (e, count) => this.updateTodoCount(count)); this.renderState(); + this.setSidebarHeight(); } handleClickEvent(e) { @@ -64,6 +67,14 @@ this.renderState(); } + setSidebarHeight() { + const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); + const diff = $navHeight - $('body').scrollTop(); + if (diff > 0) { + $('.right-sidebar').outerHeight($(window).height() - diff); + } + } + togglePinnedState() { this.isPinned = !this.isPinned; if (!this.isPinned) { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 130103a2702..da5c44b5fdc 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -190,7 +190,6 @@ .right-sidebar { position: absolute; - height: 100%; top: $header-height; bottom: 0; right: 0; From 4e334538478da577fabf070c493530189c7613cb Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Mon, 6 Feb 2017 16:19:08 -0600 Subject: [PATCH 387/488] Set sidebar height to 100% if at top of page --- app/assets/javascripts/sidebar.js.es6 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6 index 6c3a1f3307a..d0ddb5be099 100644 --- a/app/assets/javascripts/sidebar.js.es6 +++ b/app/assets/javascripts/sidebar.js.es6 @@ -72,6 +72,8 @@ const diff = $navHeight - $('body').scrollTop(); if (diff > 0) { $('.right-sidebar').outerHeight($(window).height() - diff); + } else { + $('.right-sidebar').outerHeight('100%'); } } From 3a1b2c9d50f4615ce9e40a8f21ef4885d8283010 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Tue, 7 Feb 2017 17:20:28 -0600 Subject: [PATCH 388/488] common_utils merge conflicts --- app/assets/javascripts/lib/utils/common_utils.js.es6 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index 0966adcfb68..a910999a440 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -127,12 +127,10 @@ gl.utils.scrollToElement = function($el) { var top = $el.offset().top; - gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height(); - gl.navLinksHeight = gl.navLinksHeight || $('.nav-links').height(); gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height(); return $('body, html').animate({ - scrollTop: top - (gl.navBarHeight + gl.navLinksHeight + gl.mrTabsHeight) + scrollTop: top - (gl.mrTabsHeight) }, 200); }; From 485ef9900b7a7e7534b3d20f5f9bef02dbc9b13b Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Wed, 8 Feb 2017 09:38:27 -0600 Subject: [PATCH 389/488] Add changelog --- changelogs/unreleased/static-navbar.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/static-navbar.yml diff --git a/changelogs/unreleased/static-navbar.yml b/changelogs/unreleased/static-navbar.yml new file mode 100644 index 00000000000..eaf478a48d0 --- /dev/null +++ b/changelogs/unreleased/static-navbar.yml @@ -0,0 +1,4 @@ +--- +title: Remove fixed positioning from top nav +merge_request: !7547 +author: From 7cbceef0a8290078eb2feb3956aa311628552229 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Wed, 8 Feb 2017 09:50:30 -0600 Subject: [PATCH 390/488] Remove right padding from navbar-collapse on large screens --- app/assets/stylesheets/framework/header.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 731ce57c245..34e010e0e8a 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -222,6 +222,10 @@ header { float: right; border-top: none; + @media (min-width: $screen-md-min) { + padding: 0; + } + @media (max-width: $screen-xs-max) { float: none; } From 5430849ca55192b78f7bfb35fdabaa3356e15035 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 9 Feb 2017 11:19:26 -0600 Subject: [PATCH 391/488] Replace accidentally deleted metaclick --- app/assets/javascripts/lib/utils/common_utils.js.es6 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index a910999a440..2d4f1d3dbe7 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -125,6 +125,14 @@ return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; }; + gl.utils.isMetaClick = function(e) { + // Identify following special clicks + // 1) Cmd + Click on Mac (e.metaKey) + // 2) Ctrl + Click on PC (e.ctrlKey) + // 3) Middle-click or Mouse Wheel Click (e.which is 2) + return e.metaKey || e.ctrlKey || e.which === 2; + }; + gl.utils.scrollToElement = function($el) { var top = $el.offset().top; gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height(); From c34517ad8b5248a6c40eb59152347c45ac0d0ec8 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 9 Feb 2017 13:22:38 -0600 Subject: [PATCH 392/488] Add js prefix to right sidebar --- app/assets/javascripts/lib/utils/common_utils.js.es6 | 3 +++ app/assets/javascripts/sidebar.js.es6 | 4 ++-- app/views/projects/builds/_sidebar.html.haml | 2 +- app/views/projects/wikis/_sidebar.html.haml | 2 +- app/views/shared/issuable/_sidebar.html.haml | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index 2d4f1d3dbe7..bcb3a706b51 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -69,6 +69,9 @@ var hash = w.gl.utils.getLocationHash(); if (!hash) return; + // This is required to handle non-unicode characters in hash + hash = decodeURIComponent(hash); + // scroll to user-generated markdown anchor if we cannot find a match if (document.getElementById(hash) === null) { var target = document.getElementById('user-content-' + hash); diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6 index d0ddb5be099..33e4b7db681 100644 --- a/app/assets/javascripts/sidebar.js.es6 +++ b/app/assets/javascripts/sidebar.js.es6 @@ -71,9 +71,9 @@ const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); const diff = $navHeight - $('body').scrollTop(); if (diff > 0) { - $('.right-sidebar').outerHeight($(window).height() - diff); + $('.js-right-sidebar').outerHeight($(window).height() - diff); } else { - $('.right-sidebar').outerHeight('100%'); + $('.js-right-sidebar').outerHeight('100%'); } } diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index f4a49ba71c6..78720d88e4e 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -1,6 +1,6 @@ - builds = @build.pipeline.builds.to_a -%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar{ data: { "offset-top" => "151", "spy" => "affix" } } +%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "151", "spy" => "affix" } } .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default Job %strong ##{@build.id} diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index 456477824e5..f115f60088c 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -1,4 +1,4 @@ -%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } } +%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } } .block.wiki-sidebar-header.append-bottom-default %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" } = icon('angle-double-right') diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 1ccb09c36f5..3f7f1a86b9f 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -2,7 +2,7 @@ - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('issuable') -%aside.right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } +%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } .issuable-sidebar - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .block.issuable-sidebar-header From cb8d031e959588c02efcd245ba914c07dab7f993 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 30 Nov 2016 15:07:42 +0800 Subject: [PATCH 393/488] Explicitly disable the RSpec/BeEql cop This is a little too picky, even for us. --- .rubocop.yml | 4 ++++ .rubocop_todo.yml | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 88345373a5b..e73597adca2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -769,6 +769,10 @@ Rails/ScopeArgs: RSpec/AnyInstance: Enabled: false +# Check for expectations where `be(...)` can replace `eql(...)`. +RSpec/BeEql: + Enabled: false + # Check that the first argument to the top level describe is the tested class or # module. RSpec/DescribeClass: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d581610162f..c86cd714723 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -80,10 +80,6 @@ Performance/RedundantMatch: Performance/RedundantMerge: Enabled: false -# Offense count: 7 -RSpec/BeEql: - Enabled: false - # Offense count: 15 # Configuration parameters: CustomIncludeMethods. RSpec/EmptyExampleGroup: From 92cbc1e4ad8d874428089c4c65293fa218f67206 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 30 Nov 2016 15:13:42 +0800 Subject: [PATCH 394/488] Enable the `RSpec/ExpectActual` cop and correct offenses --- .rubocop.yml | 4 ++ .rubocop_todo.yml | 4 -- .../filtered_search/filter_issues_spec.rb | 33 +++------ spec/lib/gitlab/regex_spec.rb | 71 ++++++++++--------- 4 files changed, 53 insertions(+), 59 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index e73597adca2..21ea8372e4b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -801,6 +801,10 @@ RSpec/ExampleWording: not: does not IgnoredWords: [] +# Checks for `expect(...)` calls containing literal values. +RSpec/ExpectActual: + Enabled: true + # Checks the file and folder naming of the spec file. RSpec/FilePath: Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index c86cd714723..4714b64b896 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -85,10 +85,6 @@ Performance/RedundantMerge: RSpec/EmptyExampleGroup: Enabled: false -# Offense count: 24 -RSpec/ExpectActual: - Enabled: false - # Offense count: 58 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: implicit, each, example diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 6f7046c8461..64f448a83b7 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -113,13 +113,11 @@ describe 'Filter issues', js: true, feature: true do end it 'filters issues by invalid author' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end it 'filters issues by multiple authors' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end end @@ -158,8 +156,7 @@ describe 'Filter issues', js: true, feature: true do end it 'sorting' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end end @@ -182,13 +179,11 @@ describe 'Filter issues', js: true, feature: true do end it 'filters issues by invalid assignee' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end it 'filters issues by multiple assignees' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end end @@ -228,8 +223,7 @@ describe 'Filter issues', js: true, feature: true do context 'sorting' do it 'sorts' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end end end @@ -253,8 +247,7 @@ describe 'Filter issues', js: true, feature: true do end it 'filters issues by invalid label' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end it 'filters issues by multiple labels' do @@ -429,8 +422,7 @@ describe 'Filter issues', js: true, feature: true do context 'sorting' do it 'sorts' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end end end @@ -456,13 +448,11 @@ describe 'Filter issues', js: true, feature: true do end it 'filters issues by invalid milestones' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end it 'filters issues by multiple milestones' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end it 'filters issues by milestone containing special characters' do @@ -523,8 +513,7 @@ describe 'Filter issues', js: true, feature: true do context 'sorting' do it 'sorts' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end end end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index c78cd30157e..1dbc2f6eb13 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -2,47 +2,52 @@ require 'spec_helper' describe Gitlab::Regex, lib: true do - describe 'project path regex' do - it { expect('gitlab-ce').to match(Gitlab::Regex.project_path_regex) } - it { expect('gitlab_git').to match(Gitlab::Regex.project_path_regex) } - it { expect('_underscore.js').to match(Gitlab::Regex.project_path_regex) } - it { expect('100px.com').to match(Gitlab::Regex.project_path_regex) } - it { expect('?gitlab').not_to match(Gitlab::Regex.project_path_regex) } - it { expect('git lab').not_to match(Gitlab::Regex.project_path_regex) } - it { expect('gitlab.git').not_to match(Gitlab::Regex.project_path_regex) } + describe '.project_path_regex' do + subject { described_class.project_path_regex } + + it { is_expected.to match('gitlab-ce') } + it { is_expected.to match('gitlab_git') } + it { is_expected.to match('_underscore.js') } + it { is_expected.to match('100px.com') } + it { is_expected.not_to match('?gitlab') } + it { is_expected.not_to match('git lab') } + it { is_expected.not_to match('gitlab.git') } end - describe 'project name regex' do - it { expect('gitlab-ce').to match(Gitlab::Regex.project_name_regex) } - it { expect('GitLab CE').to match(Gitlab::Regex.project_name_regex) } - it { expect('100 lines').to match(Gitlab::Regex.project_name_regex) } - it { expect('gitlab.git').to match(Gitlab::Regex.project_name_regex) } - it { expect('Český název').to match(Gitlab::Regex.project_name_regex) } - it { expect('Dash – is this').to match(Gitlab::Regex.project_name_regex) } - it { expect('?gitlab').not_to match(Gitlab::Regex.project_name_regex) } + describe '.project_name_regex' do + subject { described_class.project_name_regex } + + it { is_expected.to match('gitlab-ce') } + it { is_expected.to match('GitLab CE') } + it { is_expected.to match('100 lines') } + it { is_expected.to match('gitlab.git') } + it { is_expected.to match('Český název') } + it { is_expected.to match('Dash – is this') } + it { is_expected.not_to match('?gitlab') } end - describe 'file name regex' do - it { expect('foo@bar').to match(Gitlab::Regex.file_name_regex) } + describe '.file_name_regex' do + subject { described_class.file_name_regex } + + it { is_expected.to match('foo@bar') } end - describe 'file path regex' do - it { expect('foo@/bar').to match(Gitlab::Regex.file_path_regex) } + describe '.file_path_regex' do + subject { described_class.file_path_regex } + + it { is_expected.to match('foo@/bar') } end - describe 'environment slug regex' do - def be_matched - match(Gitlab::Regex.environment_slug_regex) - end + describe '.environment_slug_regex' do + subject { described_class.environment_slug_regex } - it { expect('foo').to be_matched } - it { expect('foo-1').to be_matched } - - it { expect('FOO').not_to be_matched } - it { expect('foo/1').not_to be_matched } - it { expect('foo.1').not_to be_matched } - it { expect('foo*1').not_to be_matched } - it { expect('9foo').not_to be_matched } - it { expect('foo-').not_to be_matched } + it { is_expected.to match('foo') } + it { is_expected.to match('foo-1') } + it { is_expected.not_to match('FOO') } + it { is_expected.not_to match('foo/1') } + it { is_expected.not_to match('foo.1') } + it { is_expected.not_to match('foo*1') } + it { is_expected.not_to match('9foo') } + it { is_expected.not_to match('foo-') } end end From 5db56efe424c9cd760580a755ec4e131d045769d Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 30 Nov 2016 15:34:29 +0800 Subject: [PATCH 395/488] Enable `Style/RedundantException` cop and correct offense --- .rubocop.yml | 4 ++++ .rubocop_todo.yml | 5 ----- app/helpers/preferences_helper.rb | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 21ea8372e4b..1061de7c797 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -339,6 +339,10 @@ Style/OpMethod: Style/ParenthesesAroundCondition: Enabled: true +# Checks for an obsolete RuntimeException argument in raise/fail. +Style/RedundantException: + Enabled: true + # Checks for parentheses that seem not to serve any purpose. Style/RedundantParentheses: Enabled: true diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 4714b64b896..7ef5523de4b 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -416,11 +416,6 @@ Style/RaiseArgs: Style/RedundantBegin: Enabled: false -# Offense count: 1 -# Cop supports --auto-correct. -Style/RedundantException: - Enabled: false - # Offense count: 29 # Cop supports --auto-correct. Style/RedundantFreeze: diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 6e68aad4cb7..dd0a4ea03f0 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -23,7 +23,7 @@ module PreferencesHelper if defined.size != DASHBOARD_CHOICES.size # Ensure that anyone adding new options updates this method too - raise RuntimeError, "`User` defines #{defined.size} dashboard choices," + + raise "`User` defines #{defined.size} dashboard choices," \ " but `DASHBOARD_CHOICES` defined #{DASHBOARD_CHOICES.size}." else defined.map do |key, _| From c0fb47333a1336000a8ae605ef23572bb38d674a Mon Sep 17 00:00:00 2001 From: Simon Knox Date: Fri, 10 Feb 2017 13:16:38 +1100 Subject: [PATCH 396/488] only print errors and warnings from webpack dev server --- config/webpack.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index 00f448c1fbb..2ac779c8511 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -117,7 +117,8 @@ if (IS_PRODUCTION) { if (IS_DEV_SERVER) { config.devServer = { port: DEV_SERVER_PORT, - headers: { 'Access-Control-Allow-Origin': '*' } + headers: { 'Access-Control-Allow-Origin': '*' }, + stats: 'errors-only', }; config.output.publicPath = '//localhost:' + DEV_SERVER_PORT + config.output.publicPath; } From 0ca691751f516b5ee2a81a8ddedd64b8be287ec6 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 10 Feb 2017 20:20:57 -0500 Subject: [PATCH 397/488] Remove animation for icons on filter dropdown --- app/assets/stylesheets/framework/animations.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 0a26b4c6a8c..0ca5a9343f7 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -128,8 +128,7 @@ .note-action-button .link-highlight, .toolbar-btn, -.dropdown-toggle-caret, -.fa:not(.fa-bell) { +.dropdown-toggle-caret { @include transition(color); } From 2a03f35461eeba8545a775b04751e6c7b866f8c0 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 10 Feb 2017 20:21:18 -0500 Subject: [PATCH 398/488] Disable animation on avatars inside filter dropdowns --- app/assets/stylesheets/framework/avatar.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 1d59700543c..3f5b78ed445 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -28,6 +28,8 @@ .avatar { @extend .avatar-circle; + @include transition-property(none); + width: 40px; height: 40px; padding: 0; From f82f3bf22dfe11809041a8bb79972f672096916e Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 10 Feb 2017 22:13:13 -0500 Subject: [PATCH 399/488] Stylize blockquote in notification emails --- app/assets/stylesheets/mailers/highlighted_diff_email.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss index 60ff72c703e..ea40f449134 100644 --- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss +++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss @@ -138,6 +138,13 @@ pre { margin: 0; } +blockquote { + color: $gl-grayish-blue; + padding: 0 0 0 15px; + margin: 0; + border-left: 3px solid $white-dark; +} + span.highlight_word { background-color: $highlighted-highlight-word !important; } From cb40ed82e75e7e168a0788b198a11114fb61903b Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 10 Feb 2017 22:14:51 -0500 Subject: [PATCH 400/488] Add CHANGELOG file --- .../28029-improve-blockquote-formatting-on-emails.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml diff --git a/changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml b/changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml new file mode 100644 index 00000000000..be2a0afbc52 --- /dev/null +++ b/changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml @@ -0,0 +1,4 @@ +--- +title: Improve blockquote formatting in notification emails +merge_request: +author: From 44826649b3f0e5d5d2c37e506eef7f5e8a0a724a Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 10 Feb 2017 10:37:16 -0500 Subject: [PATCH 401/488] Add margin to loading icon in Merge Request Widget --- app/assets/stylesheets/pages/merge_requests.scss | 8 ++++++-- ...ween-loading-icon-and-text-in-merge-request-widget.yml | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index c02a65b0903..0b0c4bc130d 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -85,14 +85,18 @@ -webkit-align-items: center; align-items: center; + i, + svg { + margin-right: 8px; + } + svg { - margin-right: 4px; position: relative; top: 1px; overflow: visible; } - &> span { + & > span { padding-right: 4px; } diff --git a/changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml b/changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml new file mode 100644 index 00000000000..1dfabd3813b --- /dev/null +++ b/changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml @@ -0,0 +1,4 @@ +--- +title: Add space between text and loading icon in Megre Request Widget +merge_request: 9119 +author: From 50559f72b52b75a3004eba4cbb376e56bb652a7e Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 6 Feb 2017 00:50:23 -0600 Subject: [PATCH 402/488] remove vue from vendor since it is now in node_modules --- .../assets/javascripts/vue-resource.full.js | 1318 --- vendor/assets/javascripts/vue-resource.js.erb | 2 - vendor/assets/javascripts/vue-resource.min.js | 7 - vendor/assets/javascripts/vue.full.js | 7515 ----------------- vendor/assets/javascripts/vue.js.erb | 2 - vendor/assets/javascripts/vue.min.js | 7 - 6 files changed, 8851 deletions(-) delete mode 100644 vendor/assets/javascripts/vue-resource.full.js delete mode 100644 vendor/assets/javascripts/vue-resource.js.erb delete mode 100644 vendor/assets/javascripts/vue-resource.min.js delete mode 100644 vendor/assets/javascripts/vue.full.js delete mode 100644 vendor/assets/javascripts/vue.js.erb delete mode 100644 vendor/assets/javascripts/vue.min.js diff --git a/vendor/assets/javascripts/vue-resource.full.js b/vendor/assets/javascripts/vue-resource.full.js deleted file mode 100644 index d7981dbec7e..00000000000 --- a/vendor/assets/javascripts/vue-resource.full.js +++ /dev/null @@ -1,1318 +0,0 @@ -/*! - * 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 deleted file mode 100644 index 8001775ce98..00000000000 --- a/vendor/assets/javascripts/vue-resource.js.erb +++ /dev/null @@ -1,2 +0,0 @@ -<% 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 deleted file mode 100644 index 6bff73a2a67..00000000000 --- a/vendor/assets/javascripts/vue-resource.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * 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=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 deleted file mode 100644 index ea15bfac416..00000000000 --- a/vendor/assets/javascripts/vue.full.js +++ /dev/null @@ -1,7515 +0,0 @@ -/*! - * Vue.js v2.0.3 - * (c) 2014-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'; - -/* */ - -/** - * Convert a value to a string that is actually rendered. - */ -function _toString (val) { - return val == null - ? '' - : typeof val === 'object' - ? JSON.stringify(val, null, 2) - : String(val) -} - -/** - * Convert a input value to a number for persistence. - * If the conversion fails, return original string. - */ -function toNumber (val) { - var n = parseFloat(val, 10); - return (n || n === 0) ? n : val -} - -/** - * Make a map and return a function for checking if a key - * is in that map. - */ -function makeMap ( - str, - expectsLowerCase -) { - var map = Object.create(null); - var list = str.split(','); - for (var i = 0; i < list.length; i++) { - map[list[i]] = true; - } - return expectsLowerCase - ? function (val) { return map[val.toLowerCase()]; } - : function (val) { return map[val]; } -} - -/** - * Check if a tag is a built-in tag. - */ -var isBuiltInTag = makeMap('slot,component', true); - -/** - * Remove an item from an array - */ -function remove$1 (arr, item) { - if (arr.length) { - var index = arr.indexOf(item); - if (index > -1) { - return arr.splice(index, 1) - } - } -} - -/** - * Check whether the object has the property. - */ -var hasOwnProperty = Object.prototype.hasOwnProperty; -function hasOwn (obj, key) { - return hasOwnProperty.call(obj, key) -} - -/** - * Check if value is primitive - */ -function isPrimitive (value) { - return typeof value === 'string' || typeof value === 'number' -} - -/** - * Create a cached version of a pure function. - */ -function cached (fn) { - var cache = Object.create(null); - return function cachedFn (str) { - var hit = cache[str]; - return hit || (cache[str] = fn(str)) - } -} - -/** - * Camelize a hyphen-delmited string. - */ -var camelizeRE = /-(\w)/g; -var camelize = cached(function (str) { - return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; }) -}); - -/** - * Capitalize a string. - */ -var capitalize = cached(function (str) { - return str.charAt(0).toUpperCase() + str.slice(1) -}); - -/** - * Hyphenate a camelCase string. - */ -var hyphenateRE = /([^-])([A-Z])/g; -var hyphenate = cached(function (str) { - return str - .replace(hyphenateRE, '$1-$2') - .replace(hyphenateRE, '$1-$2') - .toLowerCase() -}); - -/** - * Simple bind, faster than native - */ -function bind$1 (fn, ctx) { - function boundFn (a) { - var l = arguments.length; - return l - ? l > 1 - ? fn.apply(ctx, arguments) - : fn.call(ctx, a) - : fn.call(ctx) - } - // record original fn length - boundFn._length = fn.length; - return boundFn -} - -/** - * Convert an Array-like object to a real 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. - */ -function extend (to, _from) { - for (var key in _from) { - to[key] = _from[key]; - } - 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. - */ -function isObject (obj) { - return obj !== null && typeof obj === 'object' -} - -/** - * Strict object type check. Only returns true - * for plain JavaScript objects. - */ -var toString = Object.prototype.toString; -var OBJECT_STRING = '[object Object]'; -function isPlainObject (obj) { - return toString.call(obj) === OBJECT_STRING -} - -/** - * Merge an Array of Objects into a single Object. - */ -function toObject (arr) { - var res = {}; - for (var i = 0; i < arr.length; i++) { - if (arr[i]) { - extend(res, arr[i]); - } - } - return res -} - -/** - * Perform no operation. - */ -function noop () {} - -/** - * Always return false. - */ -var no = function () { return false; }; - -/** - * Generate a static keys string from compiler modules. - */ -function genStaticKeys (modules) { - return modules.reduce(function (keys, m) { - return keys.concat(m.staticKeys || []) - }, []).join(',') -} - -/** - * Check if two values are loosely equal - that is, - * if they are plain objects, do they have the same shape? - */ -function looseEqual (a, b) { - /* eslint-disable eqeqeq */ - return a == b || ( - isObject(a) && isObject(b) - ? JSON.stringify(a) === JSON.stringify(b) - : false - ) - /* eslint-enable eqeqeq */ -} - -function looseIndexOf (arr, val) { - for (var i = 0; i < arr.length; i++) { - if (looseEqual(arr[i], val)) { return i } - } - return -1 -} - -/* */ - -var config = { - /** - * Option merge strategies (used in core/util/options) - */ - optionMergeStrategies: Object.create(null), - - /** - * Whether to suppress warnings. - */ - silent: false, - - /** - * Whether to enable devtools - */ - devtools: "development" !== 'production', - - /** - * Error handler for watcher errors - */ - errorHandler: null, - - /** - * Ignore certain custom elements - */ - ignoredElements: null, - - /** - * Custom user key aliases for v-on - */ - keyCodes: Object.create(null), - - /** - * Check if a tag is reserved so that it cannot be registered as a - * component. This is platform-dependent and may be overwritten. - */ - isReservedTag: no, - - /** - * Check if a tag is an unknown element. - * Platform-dependent. - */ - isUnknownElement: no, - - /** - * Get the namespace of an element - */ - getTagNamespace: noop, - - /** - * Check if an attribute must be bound using property, e.g. value - * Platform-dependent. - */ - mustUseProp: no, - - /** - * List of asset types that a component can own. - */ - _assetTypes: [ - 'component', - 'directive', - 'filter' - ], - - /** - * List of lifecycle hooks. - */ - _lifecycleHooks: [ - 'beforeCreate', - 'created', - 'beforeMount', - 'mounted', - 'beforeUpdate', - 'updated', - 'beforeDestroy', - 'destroyed', - 'activated', - 'deactivated' - ], - - /** - * Max circular updates allowed in a scheduler flush cycle. - */ - _maxUpdateCount: 100, - - /** - * Server rendering? - */ - _isServer: "client" === 'server' -}; - -/* */ - -/** - * Check if a string starts with $ or _ - */ -function isReserved (str) { - var c = (str + '').charCodeAt(0); - return c === 0x24 || c === 0x5F -} - -/** - * Define a property. - */ -function def (obj, key, val, enumerable) { - Object.defineProperty(obj, key, { - value: val, - enumerable: !!enumerable, - writable: true, - configurable: true - }); -} - -/** - * Parse simple path. - */ -var bailRE = /[^\w\.\$]/; -function parsePath (path) { - if (bailRE.test(path)) { - return - } else { - var segments = path.split('.'); - return function (obj) { - for (var i = 0; i < segments.length; i++) { - if (!obj) { return } - obj = obj[segments[i]]; - } - return obj - } - } -} - -/* */ -/* globals MutationObserver */ - -// can we use __proto__? -var hasProto = '__proto__' in {}; - -// Browser environment sniffing -var inBrowser = - typeof window !== 'undefined' && - Object.prototype.toString.call(window) !== '[object Object]'; - -var UA = inBrowser && window.navigator.userAgent.toLowerCase(); -var isIE = UA && /msie|trident/.test(UA); -var isIE9 = UA && UA.indexOf('msie 9.0') > 0; -var isEdge = UA && UA.indexOf('edge/') > 0; -var isAndroid = UA && UA.indexOf('android') > 0; -var isIOS = UA && /iphone|ipad|ipod|ios/.test(UA); - -// detect devtools -var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; - -/* istanbul ignore next */ -function isNative (Ctor) { - return /native code/.test(Ctor.toString()) -} - -/** - * Defer a task to execute it asynchronously. - */ -var nextTick = (function () { - var callbacks = []; - var pending = false; - var timerFunc; - - function nextTickHandler () { - pending = false; - var copies = callbacks.slice(0); - callbacks.length = 0; - for (var i = 0; i < copies.length; i++) { - copies[i](); - } - } - - // the nextTick behavior leverages the microtask queue, which can be accessed - // via either native Promise.then or MutationObserver. - // MutationObserver has wider support, however it is seriously bugged in - // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It - // completely stops working after triggering a few times... so, if native - // Promise is available, we will use it: - /* istanbul ignore if */ - if (typeof Promise !== 'undefined' && isNative(Promise)) { - var p = Promise.resolve(); - timerFunc = function () { - p.then(nextTickHandler); - // in problematic UIWebViews, Promise.then doesn't completely break, but - // it can get stuck in a weird state where callbacks are pushed into the - // microtask queue but the queue isn't being flushed, until the browser - // needs to do some other work, e.g. handle a timer. Therefore we can - // "force" the microtask queue to be flushed by adding an empty timer. - if (isIOS) { setTimeout(noop); } - }; - } else if (typeof MutationObserver !== 'undefined' && ( - isNative(MutationObserver) || - // PhantomJS and iOS 7.x - MutationObserver.toString() === '[object MutationObserverConstructor]' - )) { - // use MutationObserver where native Promise is not available, - // e.g. PhantomJS IE11, iOS7, Android 4.4 - var counter = 1; - var observer = new MutationObserver(nextTickHandler); - var textNode = document.createTextNode(String(counter)); - observer.observe(textNode, { - characterData: true - }); - timerFunc = function () { - counter = (counter + 1) % 2; - textNode.data = String(counter); - }; - } else { - // fallback to setTimeout - /* istanbul ignore next */ - timerFunc = function () { - setTimeout(nextTickHandler, 0); - }; - } - - return function queueNextTick (cb, ctx) { - var func = ctx - ? function () { cb.call(ctx); } - : cb; - callbacks.push(func); - if (!pending) { - pending = true; - timerFunc(); - } - } -})(); - -var _Set; -/* istanbul ignore if */ -if (typeof Set !== 'undefined' && isNative(Set)) { - // use native Set when available. - _Set = Set; -} else { - // a non-standard Set polyfill that only works with primitive keys. - _Set = (function () { - function Set () { - this.set = Object.create(null); - } - Set.prototype.has = function has (key) { - return this.set[key] !== undefined - }; - Set.prototype.add = function add (key) { - this.set[key] = 1; - }; - Set.prototype.clear = function clear () { - this.set = Object.create(null); - }; - - return Set; - }()); -} - -/* not type checking this file because flow doesn't play well with Proxy */ - -var hasProxy; -var proxyHandlers; -var initProxy; - -{ - var allowedGlobals = makeMap( - 'Infinity,undefined,NaN,isFinite,isNaN,' + - 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + - 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + - 'require' // for Webpack/Browserify - ); - - hasProxy = - typeof Proxy !== 'undefined' && - Proxy.toString().match(/native code/); - - proxyHandlers = { - has: function has (target, key) { - var has = key in target; - var isAllowed = allowedGlobals(key) || key.charAt(0) === '_'; - if (!has && !isAllowed) { - warn( - "Property or method \"" + key + "\" is not defined on the instance but " + - "referenced during render. Make sure to declare reactive data " + - "properties in the data option.", - target - ); - } - return has || !isAllowed - } - }; - - initProxy = function initProxy (vm) { - if (hasProxy) { - vm._renderProxy = new Proxy(vm, proxyHandlers); - } else { - vm._renderProxy = vm; - } - }; -} - -/* */ - - -var uid$2 = 0; - -/** - * A dep is an observable that can have multiple - * directives subscribing to it. - */ -var Dep = function Dep () { - this.id = uid$2++; - this.subs = []; -}; - -Dep.prototype.addSub = function addSub (sub) { - this.subs.push(sub); -}; - -Dep.prototype.removeSub = function removeSub (sub) { - remove$1(this.subs, sub); -}; - -Dep.prototype.depend = function depend () { - if (Dep.target) { - Dep.target.addDep(this); - } -}; - -Dep.prototype.notify = function notify () { - // stablize the subscriber list first - var subs = this.subs.slice(); - for (var i = 0, l = subs.length; i < l; i++) { - subs[i].update(); - } -}; - -// 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; -var targetStack = []; - -function pushTarget (_target) { - if (Dep.target) { targetStack.push(Dep.target); } - Dep.target = _target; -} - -function popTarget () { - Dep.target = targetStack.pop(); -} - -/* */ - - -var queue = []; -var has$1 = {}; -var circular = {}; -var waiting = false; -var flushing = false; -var index = 0; - -/** - * Reset the scheduler's state. - */ -function resetSchedulerState () { - queue.length = 0; - has$1 = {}; - { - circular = {}; - } - waiting = flushing = false; -} - -/** - * Flush both queues and run the watchers. - */ -function flushSchedulerQueue () { - flushing = true; - - // Sort queue before flush. - // This ensures that: - // 1. Components are updated from parent to child. (because parent is always - // created before the child) - // 2. A component's user watchers are run before its render watcher (because - // user watchers are created before the render watcher) - // 3. If a component is destroyed during a parent component's watcher run, - // its watchers can be skipped. - queue.sort(function (a, b) { return a.id - b.id; }); - - // do not cache length because more watchers might be pushed - // as we run existing watchers - for (index = 0; index < queue.length; index++) { - var watcher = queue[index]; - var id = watcher.id; - has$1[id] = null; - watcher.run(); - // in dev build, check and stop circular updates. - if ("development" !== 'production' && has$1[id] != null) { - circular[id] = (circular[id] || 0) + 1; - if (circular[id] > config._maxUpdateCount) { - warn( - 'You may have an infinite update loop ' + ( - watcher.user - ? ("in watcher with expression \"" + (watcher.expression) + "\"") - : "in a component render function." - ), - watcher.vm - ); - break - } - } - } - - // devtool hook - /* istanbul ignore if */ - if (devtools && config.devtools) { - devtools.emit('flush'); - } - - resetSchedulerState(); -} - -/** - * Push a watcher into the watcher queue. - * Jobs with duplicate IDs will be skipped unless it's - * pushed when the queue is being flushed. - */ -function queueWatcher (watcher) { - var id = watcher.id; - if (has$1[id] == null) { - has$1[id] = true; - if (!flushing) { - queue.push(watcher); - } else { - // if already flushing, splice the watcher based on its id - // if already past its id, it will be run next immediately. - var i = queue.length - 1; - while (i >= 0 && queue[i].id > watcher.id) { - i--; - } - queue.splice(Math.max(i, index) + 1, 0, watcher); - } - // queue the flush - if (!waiting) { - waiting = true; - nextTick(flushSchedulerQueue); - } - } -} - -/* */ - -var uid$1 = 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. - */ -var Watcher = function Watcher ( - vm, - expOrFn, - cb, - options -) { - if ( options === void 0 ) options = {}; - - this.vm = vm; - vm._watchers.push(this); - // options - this.deep = !!options.deep; - this.user = !!options.user; - this.lazy = !!options.lazy; - this.sync = !!options.sync; - this.expression = expOrFn.toString(); - this.cb = cb; - this.id = ++uid$1; // 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(); - // parse expression for getter - if (typeof expOrFn === 'function') { - this.getter = expOrFn; - } else { - this.getter = parsePath(expOrFn); - if (!this.getter) { - this.getter = function () {}; - "development" !== 'production' && warn( - "Failed watching path: \"" + expOrFn + "\" " + - 'Watcher only accepts simple dot-delimited paths. ' + - 'For full control, use a function instead.', - vm - ); - } - } - this.value = this.lazy - ? undefined - : this.get(); -}; - -/** - * Evaluate the getter, and re-collect dependencies. - */ -Watcher.prototype.get = function get () { - pushTarget(this); - var value = this.getter.call(this.vm, this.vm); - // "touch" every property so they are all tracked as - // dependencies for deep watching - if (this.deep) { - traverse(value); - } - popTarget(); - this.cleanupDeps(); - return value -}; - -/** - * Add a dependency to this directive. - */ -Watcher.prototype.addDep = function addDep (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.cleanupDeps = function cleanupDeps () { - var this$1 = this; - - var i = this.deps.length; - while (i--) { - var dep = this$1.deps[i]; - if (!this$1.newDepIds.has(dep.id)) { - dep.removeSub(this$1); - } - } - 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. - */ -Watcher.prototype.update = function update () { - /* istanbul ignore else */ - if (this.lazy) { - this.dirty = true; - } else if (this.sync) { - this.run(); - } else { - queueWatcher(this); - } -}; - -/** - * Scheduler job interface. - * Will be called by the scheduler. - */ -Watcher.prototype.run = function run () { - 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. - isObject(value) || - this.deep - ) { - // set new value - var oldValue = this.value; - this.value = value; - if (this.user) { - try { - this.cb.call(this.vm, value, oldValue); - } catch (e) { - "development" !== 'production' && warn( - ("Error in watcher \"" + (this.expression) + "\""), - this.vm - ); - /* istanbul ignore else */ - if (config.errorHandler) { - config.errorHandler.call(null, e, this.vm); - } else { - throw e - } - } - } else { - this.cb.call(this.vm, value, oldValue); - } - } - } -}; - -/** - * Evaluate the value of the watcher. - * This only gets called for lazy watchers. - */ -Watcher.prototype.evaluate = function evaluate () { - this.value = this.get(); - this.dirty = false; -}; - -/** - * Depend on all deps collected by this watcher. - */ -Watcher.prototype.depend = function depend () { - var this$1 = this; - - var i = this.deps.length; - while (i--) { - this$1.deps[i].depend(); - } -}; - -/** - * Remove self from all dependencies' subcriber list. - */ -Watcher.prototype.teardown = function teardown () { - var this$1 = this; - - 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) { - remove$1(this.vm._watchers, this); - } - var i = this.deps.length; - while (i--) { - this$1.deps[i].removeSub(this$1); - } - this.active = false; - } -}; - -/** - * Recursively traverse an object to evoke all converted - * getters, so that every nested property inside the object - * is collected as a "deep" dependency. - */ -var seenObjects = new _Set(); -function traverse (val, seen) { - var i, keys; - if (!seen) { - seen = seenObjects; - seen.clear(); - } - var isA = Array.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); } - } - } -} - -/* - * not type checking this file because flow doesn't play well with - * dynamically accessing methods on Array prototype - */ - -var arrayProto = Array.prototype; -var arrayMethods = Object.create(arrayProto);[ - 'push', - 'pop', - 'shift', - 'unshift', - 'splice', - 'sort', - 'reverse' -] -.forEach(function (method) { - // cache original method - var original = arrayProto[method]; - def(arrayMethods, method, function mutator () { - var arguments$1 = arguments; - - // avoid leaking arguments: - // http://jsperf.com/closure-with-arguments - var i = arguments.length; - var args = new Array(i); - while (i--) { - args[i] = arguments$1[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 - }); -}); - -/* */ - -var arrayKeys = Object.getOwnPropertyNames(arrayMethods); - -/** - * By default, when a reactive property is set, the new value is - * also converted to become reactive. However when passing down props, - * we don't want to force conversion because the value may be a nested value - * under a frozen data structure. Converting it would defeat the optimization. - */ -var observerState = { - shouldConvert: true, - isSettingProps: false -}; - -/** - * 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. - */ -var Observer = function Observer (value) { - this.value = value; - this.dep = new Dep(); - this.vmCount = 0; - def(value, '__ob__', this); - if (Array.isArray(value)) { - var augment = hasProto - ? protoAugment - : copyAugment; - augment(value, arrayMethods, arrayKeys); - this.observeArray(value); - } else { - this.walk(value); - } -}; - -/** - * Walk through each property and convert them into - * getter/setters. This method should only be called when - * value type is Object. - */ -Observer.prototype.walk = function walk (obj) { - var keys = Object.keys(obj); - for (var i = 0; i < keys.length; i++) { - defineReactive$$1(obj, keys[i], obj[keys[i]]); - } -}; - -/** - * Observe a list of Array items. - */ -Observer.prototype.observeArray = function observeArray (items) { - for (var i = 0, l = items.length; i < l; i++) { - observe(items[i]); - } -}; - -// helpers - -/** - * Augment an target Object or Array by intercepting - * the prototype chain using __proto__ - */ -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. - * - * istanbul ignore next - */ -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. - */ -function observe (value) { - if (!isObject(value)) { - return - } - var ob; - if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { - ob = value.__ob__; - } else if ( - observerState.shouldConvert && - !config._isServer && - (Array.isArray(value) || isPlainObject(value)) && - Object.isExtensible(value) && - !value._isVue - ) { - ob = new Observer(value); - } - return ob -} - -/** - * Define a reactive property on an Object. - */ -function defineReactive$$1 ( - obj, - key, - val, - customSetter -) { - 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 (Array.isArray(value)) { - dependArray(value); - } - } - return value - }, - set: function reactiveSetter (newVal) { - var value = getter ? getter.call(obj) : val; - if (newVal === value) { - return - } - if ("development" !== 'production' && customSetter) { - customSetter(); - } - if (setter) { - setter.call(obj, newVal); - } else { - val = newVal; - } - childOb = observe(newVal); - dep.notify(); - } - }); -} - -/** - * Set a property on an object. Adds the new property and - * triggers change notification if the property doesn't - * already exist. - */ -function set (obj, key, val) { - if (Array.isArray(obj)) { - obj.splice(key, 1, val); - return val - } - if (hasOwn(obj, key)) { - obj[key] = val; - return - } - var ob = obj.__ob__; - if (obj._isVue || (ob && ob.vmCount)) { - "development" !== 'production' && warn( - 'Avoid adding reactive properties to a Vue instance or its root $data ' + - 'at runtime - declare it upfront in the data option.' - ); - return - } - if (!ob) { - obj[key] = val; - return - } - defineReactive$$1(ob.value, key, val); - ob.dep.notify(); - return val -} - -/** - * Delete a property and trigger change if necessary. - */ -function del (obj, key) { - var ob = obj.__ob__; - if (obj._isVue || (ob && ob.vmCount)) { - "development" !== 'production' && warn( - 'Avoid deleting properties on a Vue instance or its root $data ' + - '- just set it to null.' - ); - return - } - if (!hasOwn(obj, key)) { - return - } - delete obj[key]; - if (!ob) { - return - } - ob.dep.notify(); -} - -/** - * Collect dependencies on array elements when the array is touched, since - * we cannot intercept array element access like property getters. - */ -function dependArray (value) { - for (var e = void 0, i = 0, l = value.length; i < l; i++) { - e = value[i]; - e && e.__ob__ && e.__ob__.dep.depend(); - if (Array.isArray(e)) { - dependArray(e); - } - } -} - -/* */ - -function initState (vm) { - vm._watchers = []; - initProps(vm); - initData(vm); - initComputed(vm); - initMethods(vm); - initWatch(vm); -} - -function initProps (vm) { - var props = vm.$options.props; - if (props) { - var propsData = vm.$options.propsData || {}; - var keys = vm.$options._propKeys = Object.keys(props); - var isRoot = !vm.$parent; - // root instance props should be converted - observerState.shouldConvert = isRoot; - var loop = function ( i ) { - var key = keys[i]; - /* istanbul ignore else */ - { - defineReactive$$1(vm, key, validateProp(key, props, propsData, vm), function () { - if (vm.$parent && !observerState.isSettingProps) { - warn( - "Avoid mutating a prop directly since the value will be " + - "overwritten whenever the parent component re-renders. " + - "Instead, use a data or computed property based on the prop's " + - "value. Prop being mutated: \"" + key + "\"", - vm - ); - } - }); - } - }; - - for (var i = 0; i < keys.length; i++) loop( i ); - observerState.shouldConvert = true; - } -} - -function initData (vm) { - var data = vm.$options.data; - data = vm._data = typeof data === 'function' - ? data.call(vm) - : data || {}; - if (!isPlainObject(data)) { - data = {}; - "development" !== 'production' && warn( - 'data functions should return an object.', - vm - ); - } - // proxy data on instance - var keys = Object.keys(data); - var props = vm.$options.props; - var i = keys.length; - while (i--) { - if (props && hasOwn(props, keys[i])) { - "development" !== 'production' && warn( - "The data property \"" + (keys[i]) + "\" is already declared as a prop. " + - "Use prop default value instead.", - vm - ); - } else { - proxy(vm, keys[i]); - } - } - // observe data - observe(data); - data.__ob__ && data.__ob__.vmCount++; -} - -var computedSharedDefinition = { - enumerable: true, - configurable: true, - get: noop, - set: noop -}; - -function initComputed (vm) { - var computed = vm.$options.computed; - if (computed) { - for (var key in computed) { - var userDef = computed[key]; - if (typeof userDef === 'function') { - computedSharedDefinition.get = makeComputedGetter(userDef, vm); - computedSharedDefinition.set = noop; - } else { - computedSharedDefinition.get = userDef.get - ? userDef.cache !== false - ? makeComputedGetter(userDef.get, vm) - : bind$1(userDef.get, vm) - : noop; - computedSharedDefinition.set = userDef.set - ? bind$1(userDef.set, vm) - : noop; - } - Object.defineProperty(vm, key, computedSharedDefinition); - } - } -} - -function makeComputedGetter (getter, owner) { - var watcher = new Watcher(owner, getter, noop, { - lazy: true - }); - return function computedGetter () { - if (watcher.dirty) { - watcher.evaluate(); - } - if (Dep.target) { - watcher.depend(); - } - return watcher.value - } -} - -function initMethods (vm) { - var methods = vm.$options.methods; - if (methods) { - for (var key in methods) { - vm[key] = methods[key] == null ? noop : bind$1(methods[key], vm); - if ("development" !== 'production' && methods[key] == null) { - warn( - "method \"" + key + "\" has an undefined value in the component definition. " + - "Did you reference the function correctly?", - vm - ); - } - } - } -} - -function initWatch (vm) { - var watch = vm.$options.watch; - if (watch) { - for (var key in watch) { - var handler = watch[key]; - if (Array.isArray(handler)) { - for (var i = 0; i < handler.length; i++) { - createWatcher(vm, key, handler[i]); - } - } else { - createWatcher(vm, key, handler); - } - } - } -} - -function createWatcher (vm, key, handler) { - var options; - if (isPlainObject(handler)) { - options = handler; - handler = handler.handler; - } - if (typeof handler === 'string') { - handler = vm[handler]; - } - vm.$watch(key, handler, options); -} - -function stateMixin (Vue) { - // flow somehow has problems with directly declared definition object - // when using Object.defineProperty, so we have to procedurally build up - // the object here. - var dataDef = {}; - dataDef.get = function () { - return this._data - }; - { - dataDef.set = function (newData) { - warn( - 'Avoid replacing instance root $data. ' + - 'Use nested data properties instead.', - this - ); - }; - } - Object.defineProperty(Vue.prototype, '$data', dataDef); - - Vue.prototype.$set = set; - Vue.prototype.$delete = del; - - Vue.prototype.$watch = function ( - expOrFn, - cb, - options - ) { - var vm = this; - options = options || {}; - options.user = true; - var watcher = new Watcher(vm, expOrFn, cb, options); - if (options.immediate) { - cb.call(vm, watcher.value); - } - return function unwatchFn () { - watcher.teardown(); - } - }; -} - -function proxy (vm, key) { - if (!isReserved(key)) { - Object.defineProperty(vm, key, { - configurable: true, - enumerable: true, - get: function proxyGetter () { - return vm._data[key] - }, - set: function proxySetter (val) { - vm._data[key] = val; - } - }); - } -} - -/* */ - -var VNode = function VNode ( - tag, - data, - children, - text, - elm, - ns, - context, - componentOptions -) { - this.tag = tag; - this.data = data; - this.children = children; - this.text = text; - this.elm = elm; - this.ns = ns; - this.context = context; - this.functionalContext = undefined; - this.key = data && data.key; - this.componentOptions = componentOptions; - this.child = undefined; - this.parent = undefined; - this.raw = false; - this.isStatic = false; - this.isRootInsert = true; - this.isComment = false; - this.isCloned = false; -}; - -var emptyVNode = function () { - var node = new VNode(); - node.text = ''; - node.isComment = true; - return node -}; - -// optimized shallow clone -// used for static nodes and slot nodes because they may be reused across -// multiple renders, cloning them avoids errors when DOM manipulations rely -// on their elm reference. -function cloneVNode (vnode) { - var cloned = new VNode( - vnode.tag, - vnode.data, - vnode.children, - vnode.text, - vnode.elm, - vnode.ns, - vnode.context, - vnode.componentOptions - ); - cloned.isStatic = vnode.isStatic; - cloned.key = vnode.key; - cloned.isCloned = true; - return cloned -} - -function cloneVNodes (vnodes) { - var res = new Array(vnodes.length); - for (var i = 0; i < vnodes.length; i++) { - res[i] = cloneVNode(vnodes[i]); - } - return res -} - -/* */ - -function mergeVNodeHook (def, hookKey, hook, key) { - key = key + hookKey; - var injectedHash = def.__injected || (def.__injected = {}); - if (!injectedHash[key]) { - injectedHash[key] = true; - var oldHook = def[hookKey]; - if (oldHook) { - def[hookKey] = function () { - oldHook.apply(this, arguments); - hook.apply(this, arguments); - }; - } else { - def[hookKey] = hook; - } - } -} - -/* */ - -function updateListeners ( - on, - oldOn, - add, - remove$$1, - vm -) { - var name, cur, old, fn, event, capture; - for (name in on) { - cur = on[name]; - old = oldOn[name]; - if (!cur) { - "development" !== 'production' && warn( - "Invalid handler for event \"" + name + "\": got " + String(cur), - vm - ); - } else if (!old) { - capture = name.charAt(0) === '!'; - event = capture ? name.slice(1) : name; - if (Array.isArray(cur)) { - add(event, (cur.invoker = arrInvoker(cur)), capture); - } else { - if (!cur.invoker) { - fn = cur; - cur = on[name] = {}; - cur.fn = fn; - cur.invoker = fnInvoker(cur); - } - add(event, cur.invoker, capture); - } - } else if (cur !== old) { - if (Array.isArray(old)) { - old.length = cur.length; - for (var i = 0; i < old.length; i++) { old[i] = cur[i]; } - on[name] = old; - } else { - old.fn = cur; - on[name] = old; - } - } - } - for (name in oldOn) { - if (!on[name]) { - event = name.charAt(0) === '!' ? name.slice(1) : name; - remove$$1(event, oldOn[name].invoker); - } - } -} - -function arrInvoker (arr) { - return function (ev) { - var arguments$1 = arguments; - - var single = arguments.length === 1; - for (var i = 0; i < arr.length; i++) { - single ? arr[i](ev) : arr[i].apply(null, arguments$1); - } - } -} - -function fnInvoker (o) { - return function (ev) { - var single = arguments.length === 1; - single ? o.fn(ev) : o.fn.apply(null, arguments); - } -} - -/* */ - -function normalizeChildren ( - children, - ns, - nestedIndex -) { - if (isPrimitive(children)) { - return [createTextVNode(children)] - } - if (Array.isArray(children)) { - var res = []; - for (var i = 0, l = children.length; i < l; i++) { - var c = children[i]; - var last = res[res.length - 1]; - // nested - if (Array.isArray(c)) { - res.push.apply(res, normalizeChildren(c, ns, ((nestedIndex || '') + "_" + i))); - } else if (isPrimitive(c)) { - if (last && last.text) { - last.text += String(c); - } else if (c !== '') { - // convert primitive to vnode - res.push(createTextVNode(c)); - } - } else if (c instanceof VNode) { - if (c.text && last && last.text) { - last.text += c.text; - } else { - // inherit parent namespace - if (ns) { - applyNS(c, ns); - } - // default key for nested array children (likely generated by v-for) - if (c.tag && c.key == null && nestedIndex != null) { - c.key = "__vlist" + nestedIndex + "_" + i + "__"; - } - res.push(c); - } - } - } - return res - } -} - -function createTextVNode (val) { - return new VNode(undefined, undefined, undefined, String(val)) -} - -function applyNS (vnode, ns) { - if (vnode.tag && !vnode.ns) { - vnode.ns = ns; - if (vnode.children) { - for (var i = 0, l = vnode.children.length; i < l; i++) { - applyNS(vnode.children[i], ns); - } - } - } -} - -/* */ - -function getFirstComponentChild (children) { - return children && children.filter(function (c) { return c && c.componentOptions; })[0] -} - -/* */ - -var activeInstance = null; - -function initLifecycle (vm) { - var options = vm.$options; - - // locate first non-abstract parent - var parent = options.parent; - if (parent && !options.abstract) { - while (parent.$options.abstract && parent.$parent) { - parent = parent.$parent; - } - parent.$children.push(vm); - } - - vm.$parent = parent; - vm.$root = parent ? parent.$root : vm; - - vm.$children = []; - vm.$refs = {}; - - vm._watcher = null; - vm._inactive = false; - vm._isMounted = false; - vm._isDestroyed = false; - vm._isBeingDestroyed = false; -} - -function lifecycleMixin (Vue) { - Vue.prototype._mount = function ( - el, - hydrating - ) { - var vm = this; - vm.$el = el; - if (!vm.$options.render) { - vm.$options.render = emptyVNode; - { - /* istanbul ignore if */ - if (vm.$options.template) { - warn( - 'You are using the runtime-only build of Vue where the template ' + - 'option is not available. Either pre-compile the templates into ' + - 'render functions, or use the compiler-included build.', - vm - ); - } else { - warn( - 'Failed to mount component: template or render function not defined.', - vm - ); - } - } - } - callHook(vm, 'beforeMount'); - vm._watcher = new Watcher(vm, function () { - vm._update(vm._render(), hydrating); - }, noop); - hydrating = false; - // manually mounted instance, call mounted on self - // mounted is called for render-created child components in its inserted hook - if (vm.$vnode == null) { - vm._isMounted = true; - callHook(vm, 'mounted'); - } - return vm - }; - - Vue.prototype._update = function (vnode, hydrating) { - var vm = this; - if (vm._isMounted) { - callHook(vm, 'beforeUpdate'); - } - var prevEl = vm.$el; - var prevActiveInstance = activeInstance; - activeInstance = vm; - var prevVnode = vm._vnode; - vm._vnode = vnode; - if (!prevVnode) { - // Vue.prototype.__patch__ is injected in entry points - // based on the rendering backend used. - vm.$el = vm.__patch__(vm.$el, vnode, hydrating); - } else { - vm.$el = vm.__patch__(prevVnode, vnode); - } - activeInstance = prevActiveInstance; - // update __vue__ reference - if (prevEl) { - prevEl.__vue__ = null; - } - if (vm.$el) { - vm.$el.__vue__ = vm; - } - // if parent is an HOC, update its $el as well - if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { - vm.$parent.$el = vm.$el; - } - if (vm._isMounted) { - callHook(vm, 'updated'); - } - }; - - Vue.prototype._updateFromParent = function ( - propsData, - listeners, - parentVnode, - renderChildren - ) { - var vm = this; - var hasChildren = !!(vm.$options._renderChildren || renderChildren); - vm.$options._parentVnode = parentVnode; - vm.$options._renderChildren = renderChildren; - // update props - if (propsData && vm.$options.props) { - observerState.shouldConvert = false; - { - observerState.isSettingProps = true; - } - var propKeys = vm.$options._propKeys || []; - for (var i = 0; i < propKeys.length; i++) { - var key = propKeys[i]; - vm[key] = validateProp(key, vm.$options.props, propsData, vm); - } - observerState.shouldConvert = true; - { - observerState.isSettingProps = false; - } - } - // update listeners - if (listeners) { - var oldListeners = vm.$options._parentListeners; - vm.$options._parentListeners = listeners; - vm._updateListeners(listeners, oldListeners); - } - // resolve slots + force update if has children - if (hasChildren) { - vm.$slots = resolveSlots(renderChildren, vm._renderContext); - vm.$forceUpdate(); - } - }; - - Vue.prototype.$forceUpdate = function () { - var vm = this; - if (vm._watcher) { - vm._watcher.update(); - } - }; - - Vue.prototype.$destroy = function () { - var vm = this; - if (vm._isBeingDestroyed) { - return - } - callHook(vm, 'beforeDestroy'); - vm._isBeingDestroyed = true; - // remove self from parent - var parent = vm.$parent; - if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) { - remove$1(parent.$children, vm); - } - // teardown watchers - if (vm._watcher) { - vm._watcher.teardown(); - } - var i = vm._watchers.length; - while (i--) { - vm._watchers[i].teardown(); - } - // remove reference from data ob - // frozen object may not have observer. - if (vm._data.__ob__) { - vm._data.__ob__.vmCount--; - } - // call the last hook... - vm._isDestroyed = true; - callHook(vm, 'destroyed'); - // turn off all instance listeners. - vm.$off(); - // remove __vue__ reference - if (vm.$el) { - vm.$el.__vue__ = null; - } - // invoke destroy hooks on current rendered tree - vm.__patch__(vm._vnode, null); - }; -} - -function callHook (vm, hook) { - var handlers = vm.$options[hook]; - if (handlers) { - for (var i = 0, j = handlers.length; i < j; i++) { - handlers[i].call(vm); - } - } - vm.$emit('hook:' + hook); -} - -/* */ - -var hooks = { init: init, prepatch: prepatch, insert: insert, destroy: destroy$1 }; -var hooksToMerge = Object.keys(hooks); - -function createComponent ( - Ctor, - data, - context, - children, - tag -) { - if (!Ctor) { - return - } - - if (isObject(Ctor)) { - Ctor = Vue$3.extend(Ctor); - } - - if (typeof Ctor !== 'function') { - { - warn(("Invalid Component definition: " + (String(Ctor))), context); - } - return - } - - // async component - if (!Ctor.cid) { - if (Ctor.resolved) { - Ctor = Ctor.resolved; - } else { - Ctor = resolveAsyncComponent(Ctor, function () { - // it's ok to queue this on every render because - // $forceUpdate is buffered by the scheduler. - context.$forceUpdate(); - }); - if (!Ctor) { - // return nothing if this is indeed an async component - // wait for the callback to trigger parent update. - return - } - } - } - - data = data || {}; - - // extract props - var propsData = extractProps(data, Ctor); - - // functional component - if (Ctor.options.functional) { - return createFunctionalComponent(Ctor, propsData, data, context, children) - } - - // extract listeners, since these needs to be treated as - // child component listeners instead of DOM listeners - var listeners = data.on; - // replace with listeners with .native modifier - data.on = data.nativeOn; - - if (Ctor.options.abstract) { - // abstract components do not keep anything - // other than props & listeners - data = {}; - } - - // merge component management hooks onto the placeholder node - mergeHooks(data); - - // return a placeholder vnode - var name = Ctor.options.name || tag; - var vnode = new VNode( - ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')), - data, undefined, undefined, undefined, undefined, context, - { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children } - ); - return vnode -} - -function createFunctionalComponent ( - Ctor, - propsData, - data, - context, - children -) { - var props = {}; - var propOptions = Ctor.options.props; - if (propOptions) { - for (var key in propOptions) { - props[key] = validateProp(key, propOptions, propsData); - } - } - var vnode = Ctor.options.render.call( - null, - // ensure the createElement function in functional components - // gets a unique context - this is necessary for correct named slot check - bind$1(createElement, { _self: Object.create(context) }), - { - props: props, - data: data, - parent: context, - children: normalizeChildren(children), - slots: function () { return resolveSlots(children, context); } - } - ); - if (vnode instanceof VNode) { - vnode.functionalContext = context; - if (data.slot) { - (vnode.data || (vnode.data = {})).slot = data.slot; - } - } - return vnode -} - -function createComponentInstanceForVnode ( - vnode, // we know it's MountedComponentVNode but flow doesn't - parent // activeInstance in lifecycle state -) { - var vnodeComponentOptions = vnode.componentOptions; - var options = { - _isComponent: true, - parent: parent, - propsData: vnodeComponentOptions.propsData, - _componentTag: vnodeComponentOptions.tag, - _parentVnode: vnode, - _parentListeners: vnodeComponentOptions.listeners, - _renderChildren: vnodeComponentOptions.children - }; - // check inline-template render functions - var inlineTemplate = vnode.data.inlineTemplate; - if (inlineTemplate) { - options.render = inlineTemplate.render; - options.staticRenderFns = inlineTemplate.staticRenderFns; - } - return new vnodeComponentOptions.Ctor(options) -} - -function init (vnode, hydrating) { - if (!vnode.child || vnode.child._isDestroyed) { - var child = vnode.child = createComponentInstanceForVnode(vnode, activeInstance); - child.$mount(hydrating ? vnode.elm : undefined, hydrating); - } -} - -function prepatch ( - oldVnode, - vnode -) { - var options = vnode.componentOptions; - var child = vnode.child = oldVnode.child; - child._updateFromParent( - options.propsData, // updated props - options.listeners, // updated listeners - vnode, // new parent vnode - options.children // new children - ); -} - -function insert (vnode) { - if (!vnode.child._isMounted) { - vnode.child._isMounted = true; - callHook(vnode.child, 'mounted'); - } - if (vnode.data.keepAlive) { - vnode.child._inactive = false; - callHook(vnode.child, 'activated'); - } -} - -function destroy$1 (vnode) { - if (!vnode.child._isDestroyed) { - if (!vnode.data.keepAlive) { - vnode.child.$destroy(); - } else { - vnode.child._inactive = true; - callHook(vnode.child, 'deactivated'); - } - } -} - -function resolveAsyncComponent ( - factory, - cb -) { - if (factory.requested) { - // pool callbacks - factory.pendingCallbacks.push(cb); - } else { - factory.requested = true; - var cbs = factory.pendingCallbacks = [cb]; - var sync = true; - - var resolve = function (res) { - if (isObject(res)) { - res = Vue$3.extend(res); - } - // cache resolved - factory.resolved = res; - // invoke callbacks only if this is not a synchronous resolve - // (async resolves are shimmed as synchronous during SSR) - if (!sync) { - for (var i = 0, l = cbs.length; i < l; i++) { - cbs[i](res); - } - } - }; - - var reject = function (reason) { - "development" !== 'production' && warn( - "Failed to resolve async component: " + (String(factory)) + - (reason ? ("\nReason: " + reason) : '') - ); - }; - - var res = factory(resolve, reject); - - // handle promise - if (res && typeof res.then === 'function' && !factory.resolved) { - res.then(resolve, reject); - } - - sync = false; - // return in case resolved synchronously - return factory.resolved - } -} - -function extractProps (data, Ctor) { - // we are only extrating raw values here. - // validation and default values are handled in the child - // component itself. - var propOptions = Ctor.options.props; - if (!propOptions) { - return - } - var res = {}; - var attrs = data.attrs; - var props = data.props; - var domProps = data.domProps; - if (attrs || props || domProps) { - for (var key in propOptions) { - var altKey = hyphenate(key); - checkProp(res, props, key, altKey, true) || - checkProp(res, attrs, key, altKey) || - checkProp(res, domProps, key, altKey); - } - } - return res -} - -function checkProp ( - res, - hash, - key, - altKey, - preserve -) { - if (hash) { - if (hasOwn(hash, key)) { - res[key] = hash[key]; - if (!preserve) { - delete hash[key]; - } - return true - } else if (hasOwn(hash, altKey)) { - res[key] = hash[altKey]; - if (!preserve) { - delete hash[altKey]; - } - return true - } - } - return false -} - -function mergeHooks (data) { - if (!data.hook) { - data.hook = {}; - } - for (var i = 0; i < hooksToMerge.length; i++) { - var key = hooksToMerge[i]; - var fromParent = data.hook[key]; - var ours = hooks[key]; - data.hook[key] = fromParent ? mergeHook$1(ours, fromParent) : ours; - } -} - -function mergeHook$1 (a, b) { - // since all hooks have at most two args, use fixed args - // to avoid having to use fn.apply(). - return function (_, __) { - a(_, __); - b(_, __); - } -} - -/* */ - -// wrapper function for providing a more flexible interface -// without getting yelled at by flow -function createElement ( - tag, - data, - children -) { - if (data && (Array.isArray(data) || typeof data !== 'object')) { - children = data; - data = undefined; - } - // make sure to use real instance instead of proxy as context - return _createElement(this._self, tag, data, children) -} - -function _createElement ( - context, - tag, - data, - children -) { - if (data && data.__ob__) { - "development" !== 'production' && warn( - "Avoid using observed data object as vnode data: " + (JSON.stringify(data)) + "\n" + - 'Always create fresh vnode data objects in each render!', - context - ); - return - } - if (!tag) { - // in case of component :is set to falsy value - return emptyVNode() - } - if (typeof tag === 'string') { - var Ctor; - var ns = config.getTagNamespace(tag); - if (config.isReservedTag(tag)) { - // platform built-in elements - return new VNode( - tag, data, normalizeChildren(children, ns), - undefined, undefined, ns, context - ) - } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) { - // component - return createComponent(Ctor, data, context, children, tag) - } else { - // unknown or unlisted namespaced elements - // check at runtime because it may get assigned a namespace when its - // parent normalizes children - return new VNode( - tag, data, normalizeChildren(children, ns), - undefined, undefined, ns, context - ) - } - } else { - // direct component options / constructor - return createComponent(tag, data, context, children) - } -} - -/* */ - -function initRender (vm) { - vm.$vnode = null; // the placeholder node in parent tree - vm._vnode = null; // the root of the child tree - vm._staticTrees = null; - vm._renderContext = vm.$options._parentVnode && vm.$options._parentVnode.context; - vm.$slots = resolveSlots(vm.$options._renderChildren, vm._renderContext); - // bind the public createElement fn to this instance - // so that we get proper render context inside it. - vm.$createElement = bind$1(createElement, vm); - if (vm.$options.el) { - vm.$mount(vm.$options.el); - } -} - -function renderMixin (Vue) { - Vue.prototype.$nextTick = function (fn) { - nextTick(fn, this); - }; - - Vue.prototype._render = function () { - var vm = this; - var ref = vm.$options; - var render = ref.render; - var staticRenderFns = ref.staticRenderFns; - var _parentVnode = ref._parentVnode; - - if (vm._isMounted) { - // clone slot nodes on re-renders - for (var key in vm.$slots) { - vm.$slots[key] = cloneVNodes(vm.$slots[key]); - } - } - - if (staticRenderFns && !vm._staticTrees) { - vm._staticTrees = []; - } - // set parent vnode. this allows render functions to have access - // to the data on the placeholder node. - vm.$vnode = _parentVnode; - // render self - var vnode; - try { - vnode = render.call(vm._renderProxy, vm.$createElement); - } catch (e) { - { - warn(("Error when rendering " + (formatComponentName(vm)) + ":")); - } - /* istanbul ignore else */ - if (config.errorHandler) { - config.errorHandler.call(null, e, vm); - } else { - if (config._isServer) { - throw e - } else { - setTimeout(function () { throw e }, 0); - } - } - // return previous vnode to prevent render error causing blank component - vnode = vm._vnode; - } - // return empty vnode in case the render function errored out - if (!(vnode instanceof VNode)) { - if ("development" !== 'production' && Array.isArray(vnode)) { - warn( - 'Multiple root nodes returned from render function. Render function ' + - 'should return a single root node.', - vm - ); - } - vnode = emptyVNode(); - } - // set parent - vnode.parent = _parentVnode; - return vnode - }; - - // shorthands used in render functions - Vue.prototype._h = createElement; - // toString for mustaches - Vue.prototype._s = _toString; - // number conversion - Vue.prototype._n = toNumber; - // empty vnode - Vue.prototype._e = emptyVNode; - // loose equal - Vue.prototype._q = looseEqual; - // loose indexOf - Vue.prototype._i = looseIndexOf; - - // render static tree by index - Vue.prototype._m = function renderStatic ( - index, - isInFor - ) { - var tree = this._staticTrees[index]; - // if has already-rendered static tree and not inside v-for, - // we can reuse the same tree by doing a shallow clone. - if (tree && !isInFor) { - return Array.isArray(tree) - ? cloneVNodes(tree) - : cloneVNode(tree) - } - // otherwise, render a fresh tree. - tree = this._staticTrees[index] = this.$options.staticRenderFns[index].call(this._renderProxy); - if (Array.isArray(tree)) { - for (var i = 0; i < tree.length; i++) { - if (typeof tree[i] !== 'string') { - tree[i].isStatic = true; - tree[i].key = "__static__" + index + "_" + i; - } - } - } else { - tree.isStatic = true; - tree.key = "__static__" + index; - } - return tree - }; - - // filter resolution helper - var identity = function (_) { return _; }; - Vue.prototype._f = function resolveFilter (id) { - return resolveAsset(this.$options, 'filters', id, true) || identity - }; - - // render v-for - Vue.prototype._l = function renderList ( - val, - render - ) { - var ret, i, l, keys, key; - if (Array.isArray(val)) { - ret = new Array(val.length); - for (i = 0, l = val.length; i < l; i++) { - ret[i] = render(val[i], i); - } - } else if (typeof val === 'number') { - ret = new Array(val); - for (i = 0; i < val; i++) { - ret[i] = render(i + 1, i); - } - } else if (isObject(val)) { - keys = Object.keys(val); - ret = new Array(keys.length); - for (i = 0, l = keys.length; i < l; i++) { - key = keys[i]; - ret[i] = render(val[key], key, i); - } - } - return ret - }; - - // renderSlot - Vue.prototype._t = function ( - name, - fallback - ) { - var slotNodes = this.$slots[name]; - // warn duplicate slot usage - if (slotNodes && "development" !== 'production') { - slotNodes._rendered && warn( - "Duplicate presence of slot \"" + name + "\" found in the same render tree " + - "- this will likely cause render errors.", - this - ); - slotNodes._rendered = true; - } - return slotNodes || fallback - }; - - // apply v-bind object - Vue.prototype._b = function bindProps ( - data, - value, - asProp - ) { - if (value) { - if (!isObject(value)) { - "development" !== 'production' && warn( - 'v-bind without argument expects an Object or Array value', - this - ); - } else { - if (Array.isArray(value)) { - value = toObject(value); - } - for (var key in value) { - if (key === 'class' || key === 'style') { - data[key] = value[key]; - } else { - var hash = asProp || config.mustUseProp(key) - ? data.domProps || (data.domProps = {}) - : data.attrs || (data.attrs = {}); - hash[key] = value[key]; - } - } - } - } - return data - }; - - // expose v-on keyCodes - Vue.prototype._k = function getKeyCodes (key) { - return config.keyCodes[key] - }; -} - -function resolveSlots ( - renderChildren, - context -) { - var slots = {}; - if (!renderChildren) { - return slots - } - var children = normalizeChildren(renderChildren) || []; - var defaultSlot = []; - var name, child; - for (var i = 0, l = children.length; i < l; i++) { - child = children[i]; - // named slots should only be respected if the vnode was rendered in the - // same context. - if ((child.context === context || child.functionalContext === context) && - child.data && (name = child.data.slot)) { - var slot = (slots[name] || (slots[name] = [])); - if (child.tag === 'template') { - slot.push.apply(slot, child.children); - } else { - slot.push(child); - } - } else { - defaultSlot.push(child); - } - } - // ignore single whitespace - if (defaultSlot.length && !( - defaultSlot.length === 1 && - (defaultSlot[0].text === ' ' || defaultSlot[0].isComment) - )) { - slots.default = defaultSlot; - } - return slots -} - -/* */ - -function initEvents (vm) { - vm._events = Object.create(null); - // init parent attached events - var listeners = vm.$options._parentListeners; - var on = bind$1(vm.$on, vm); - var off = bind$1(vm.$off, vm); - vm._updateListeners = function (listeners, oldListeners) { - updateListeners(listeners, oldListeners || {}, on, off, vm); - }; - if (listeners) { - vm._updateListeners(listeners); - } -} - -function eventsMixin (Vue) { - Vue.prototype.$on = function (event, fn) { - var vm = this;(vm._events[event] || (vm._events[event] = [])).push(fn); - return vm - }; - - Vue.prototype.$once = function (event, fn) { - var vm = this; - function on () { - vm.$off(event, on); - fn.apply(vm, arguments); - } - on.fn = fn; - vm.$on(event, on); - return vm - }; - - Vue.prototype.$off = function (event, fn) { - var vm = this; - // all - if (!arguments.length) { - vm._events = Object.create(null); - return vm - } - // specific event - var cbs = vm._events[event]; - if (!cbs) { - return vm - } - if (arguments.length === 1) { - vm._events[event] = null; - return vm - } - // specific handler - var cb; - var i = cbs.length; - while (i--) { - cb = cbs[i]; - if (cb === fn || cb.fn === fn) { - cbs.splice(i, 1); - break - } - } - return vm - }; - - Vue.prototype.$emit = function (event) { - var vm = this; - var cbs = vm._events[event]; - if (cbs) { - cbs = cbs.length > 1 ? toArray(cbs) : cbs; - var args = toArray(arguments, 1); - for (var i = 0, l = cbs.length; i < l; i++) { - cbs[i].apply(vm, args); - } - } - return vm - }; -} - -/* */ - -var uid = 0; - -function initMixin (Vue) { - Vue.prototype._init = function (options) { - var vm = this; - // a uid - vm._uid = uid++; - // a flag to avoid this being observed - vm._isVue = true; - // merge options - if (options && options._isComponent) { - // optimize internal component instantiation - // since dynamic options merging is pretty slow, and none of the - // internal component options needs special treatment. - initInternalComponent(vm, options); - } else { - vm.$options = mergeOptions( - resolveConstructorOptions(vm), - options || {}, - vm - ); - } - /* istanbul ignore else */ - { - initProxy(vm); - } - // expose real self - vm._self = vm; - initLifecycle(vm); - initEvents(vm); - callHook(vm, 'beforeCreate'); - initState(vm); - callHook(vm, 'created'); - initRender(vm); - }; - - function initInternalComponent (vm, options) { - var opts = vm.$options = Object.create(resolveConstructorOptions(vm)); - // doing this because it's faster than dynamic enumeration. - opts.parent = options.parent; - opts.propsData = options.propsData; - opts._parentVnode = options._parentVnode; - opts._parentListeners = options._parentListeners; - opts._renderChildren = options._renderChildren; - opts._componentTag = options._componentTag; - if (options.render) { - opts.render = options.render; - opts.staticRenderFns = options.staticRenderFns; - } - } - - function resolveConstructorOptions (vm) { - var Ctor = vm.constructor; - var options = Ctor.options; - if (Ctor.super) { - var superOptions = Ctor.super.options; - var cachedSuperOptions = Ctor.superOptions; - if (superOptions !== cachedSuperOptions) { - // super option changed - Ctor.superOptions = superOptions; - options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions); - if (options.name) { - options.components[options.name] = Ctor; - } - } - } - return options - } -} - -function Vue$3 (options) { - if ("development" !== 'production' && - !(this instanceof Vue$3)) { - warn('Vue is a constructor and should be called with the `new` keyword'); - } - this._init(options); -} - -initMixin(Vue$3); -stateMixin(Vue$3); -eventsMixin(Vue$3); -lifecycleMixin(Vue$3); -renderMixin(Vue$3); - -var warn = noop; -var formatComponentName; - -{ - var hasConsole = typeof console !== 'undefined'; - - warn = function (msg, vm) { - if (hasConsole && (!config.silent)) { - console.error("[Vue warn]: " + msg + " " + ( - vm ? formatLocation(formatComponentName(vm)) : '' - )); - } - }; - - formatComponentName = function (vm) { - if (vm.$root === vm) { - return 'root instance' - } - var name = vm._isVue - ? vm.$options.name || vm.$options._componentTag - : vm.name; - return ( - (name ? ("component <" + name + ">") : "anonymous component") + - (vm._isVue && vm.$options.__file ? (" at " + (vm.$options.__file)) : '') - ) - }; - - var formatLocation = function (str) { - if (str === 'anonymous component') { - str += " - use the \"name\" option for better debugging messages."; - } - return ("\n(found in " + str + ")") - }; -} - -/* */ - -/** - * Option overwriting strategies are functions that handle - * how to merge a parent option value and a child option - * value into the final value. - */ -var strats = config.optionMergeStrategies; - -/** - * Options with restrictions - */ -{ - strats.el = strats.propsData = function (parent, child, vm, key) { - if (!vm) { - warn( - "option \"" + key + "\" can only be used during instance " + - 'creation with the `new` keyword.' - ); - } - return defaultStrat(parent, child) - }; -} - -/** - * 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 - } - } - } -}; - -/** - * Hooks and param attributes are merged as arrays. - */ -function mergeHook ( - parentVal, - childVal -) { - return childVal - ? parentVal - ? parentVal.concat(childVal) - : Array.isArray(childVal) - ? childVal - : [childVal] - : parentVal -} - -config._lifecycleHooks.forEach(function (hook) { - strats[hook] = mergeHook; -}); - -/** - * 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, childVal) - : res -} - -config._assetTypes.forEach(function (type) { - strats[type + 's'] = mergeAssets; -}); - -/** - * Watchers. - * - * Watchers hashes should not overwrite one - * another, so we merge them as arrays. - */ -strats.watch = function (parentVal, childVal) { - /* istanbul ignore if */ - 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 && !Array.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 (parentVal, childVal) { - return childVal === undefined - ? parentVal - : childVal -}; - -/** - * Make sure component options get converted to actual - * constructors. - */ -function normalizeComponents (options) { - if (options.components) { - var components = options.components; - var def; - for (var key in components) { - var lower = key.toLowerCase(); - if (isBuiltInTag(lower) || config.isReservedTag(lower)) { - "development" !== 'production' && warn( - 'Do not use built-in or reserved HTML elements as component ' + - 'id: ' + key - ); - continue - } - def = components[key]; - if (isPlainObject(def)) { - components[key] = Vue$3.extend(def); - } - } - } -} - -/** - * Ensure all props option syntax are normalized into the - * Object-based format. - */ -function normalizeProps (options) { - var props = options.props; - if (!props) { return } - var res = {}; - var i, val, name; - if (Array.isArray(props)) { - i = props.length; - while (i--) { - val = props[i]; - if (typeof val === 'string') { - name = camelize(val); - res[name] = { type: null }; - } else { - warn('props must be strings when using array syntax.'); - } - } - } else if (isPlainObject(props)) { - for (var key in props) { - val = props[key]; - name = camelize(key); - res[name] = isPlainObject(val) - ? val - : { type: val }; - } - } - options.props = res; -} - -/** - * Normalize raw function directives into object format. - */ -function normalizeDirectives (options) { - var dirs = options.directives; - if (dirs) { - for (var key in dirs) { - var def = dirs[key]; - if (typeof def === 'function') { - dirs[key] = { bind: def, update: def }; - } - } - } -} - -/** - * Merge two option objects into a new one. - * Core utility used in both instantiation and inheritance. - */ -function mergeOptions ( - parent, - child, - vm -) { - normalizeComponents(child); - normalizeProps(child); - normalizeDirectives(child); - var extendsFrom = child.extends; - if (extendsFrom) { - parent = typeof extendsFrom === 'function' - ? mergeOptions(parent, extendsFrom.options, vm) - : mergeOptions(parent, extendsFrom, vm); - } - if (child.mixins) { - for (var i = 0, l = child.mixins.length; i < l; i++) { - var mixin = child.mixins[i]; - if (mixin.prototype instanceof Vue$3) { - mixin = mixin.options; - } - parent = mergeOptions(parent, mixin, vm); - } - } - var options = {}; - var key; - 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. - */ -function resolveAsset ( - options, - type, - id, - warnMissing -) { - /* istanbul ignore if */ - if (typeof id !== 'string') { - return - } - var assets = options[type]; - var res = assets[id] || - // camelCase ID - assets[camelize(id)] || - // Pascal Case ID - assets[capitalize(camelize(id))]; - if ("development" !== 'production' && warnMissing && !res) { - warn( - 'Failed to resolve ' + type.slice(0, -1) + ': ' + id, - options - ); - } - return res -} - -/* */ - -function validateProp ( - key, - propOptions, - propsData, - vm -) { - var prop = propOptions[key]; - var absent = !hasOwn(propsData, key); - var value = propsData[key]; - // handle boolean props - if (isBooleanType(prop.type)) { - if (absent && !hasOwn(prop, 'default')) { - value = false; - } else if (value === '' || value === hyphenate(key)) { - value = true; - } - } - // check default value - if (value === undefined) { - value = getPropDefaultValue(vm, prop, key); - // since the default value is a fresh copy, - // make sure to observe it. - var prevShouldConvert = observerState.shouldConvert; - observerState.shouldConvert = true; - observe(value); - observerState.shouldConvert = prevShouldConvert; - } - { - assertProp(prop, key, value, vm, absent); - } - return value -} - -/** - * Get the default value of a prop. - */ -function getPropDefaultValue (vm, prop, name) { - // no default, return undefined - if (!hasOwn(prop, 'default')) { - return undefined - } - var def = prop.default; - // warn against non-factory defaults for Object & Array - if (isObject(def)) { - "development" !== 'production' && warn( - 'Invalid default value for 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' && prop.type !== Function - ? def.call(vm) - : def -} - -/** - * Assert whether a prop is valid. - */ -function assertProp ( - prop, - name, - value, - vm, - absent -) { - if (prop.required && absent) { - warn( - 'Missing required prop: "' + name + '"', - vm - ); - return - } - if (value == null && !prop.required) { - return - } - var type = prop.type; - var valid = !type || type === true; - var expectedTypes = []; - if (type) { - if (!Array.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) { - warn( - 'Invalid prop: type check failed for prop "' + name + '".' + - ' Expected ' + expectedTypes.map(capitalize).join(', ') + - ', got ' + Object.prototype.toString.call(value).slice(8, -1) + '.', - vm - ); - return - } - var validator = prop.validator; - if (validator) { - if (!validator(value)) { - warn( - 'Invalid prop: custom validator check failed for prop "' + name + '".', - vm - ); - } - } -} - -/** - * Assert the type of a value - */ -function assertType (value, type) { - var valid; - var expectedType = getType(type); - if (expectedType === 'String') { - valid = typeof value === (expectedType = 'string'); - } else if (expectedType === 'Number') { - valid = typeof value === (expectedType = 'number'); - } else if (expectedType === 'Boolean') { - valid = typeof value === (expectedType = 'boolean'); - } else if (expectedType === 'Function') { - valid = typeof value === (expectedType = 'function'); - } else if (expectedType === 'Object') { - valid = isPlainObject(value); - } else if (expectedType === 'Array') { - valid = Array.isArray(value); - } else { - valid = value instanceof type; - } - return { - valid: valid, - expectedType: expectedType - } -} - -/** - * Use function string name to check built-in types, - * because a simple equality check will fail when running - * across different vms / iframes. - */ -function getType (fn) { - var match = fn && fn.toString().match(/^\s*function (\w+)/); - return match && match[1] -} - -function isBooleanType (fn) { - if (!Array.isArray(fn)) { - return getType(fn) === 'Boolean' - } - for (var i = 0, len = fn.length; i < len; i++) { - if (getType(fn[i]) === 'Boolean') { - return true - } - } - /* istanbul ignore next */ - return false -} - - - -var util = Object.freeze({ - defineReactive: defineReactive$$1, - _toString: _toString, - toNumber: toNumber, - makeMap: makeMap, - isBuiltInTag: isBuiltInTag, - remove: remove$1, - hasOwn: hasOwn, - isPrimitive: isPrimitive, - cached: cached, - camelize: camelize, - capitalize: capitalize, - hyphenate: hyphenate, - bind: bind$1, - toArray: toArray, - extend: extend, - isObject: isObject, - isPlainObject: isPlainObject, - toObject: toObject, - noop: noop, - no: no, - genStaticKeys: genStaticKeys, - looseEqual: looseEqual, - looseIndexOf: looseIndexOf, - isReserved: isReserved, - def: def, - parsePath: parsePath, - hasProto: hasProto, - inBrowser: inBrowser, - UA: UA, - isIE: isIE, - isIE9: isIE9, - isEdge: isEdge, - isAndroid: isAndroid, - isIOS: isIOS, - devtools: devtools, - nextTick: nextTick, - get _Set () { return _Set; }, - mergeOptions: mergeOptions, - resolveAsset: resolveAsset, - get warn () { return warn; }, - get formatComponentName () { return formatComponentName; }, - validateProp: validateProp -}); - -/* */ - -function initUse (Vue) { - 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 - }; -} - -/* */ - -function initMixin$1 (Vue) { - Vue.mixin = function (mixin) { - Vue.options = mergeOptions(Vue.options, mixin); - }; -} - -/* */ - -function initExtend (Vue) { - /** - * 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 - */ - 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 (!/^[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 = function VueComponent (options) { - this._init(options); - }; - 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; - } - // keep a reference to the super options at extension time. - // later at instantiation we can check if Super's options have - // been updated. - Sub.superOptions = Super.options; - Sub.extendOptions = extendOptions; - // cache constructor - if (isFirstExtend) { - extendOptions._Ctor = Sub; - } - return Sub - }; -} - -/* */ - -function initAssetRegisters (Vue) { - /** - * Create asset registration methods. - */ - config._assetTypes.forEach(function (type) { - Vue[type] = function ( - id, - definition - ) { - if (!definition) { - return this.options[type + 's'][id] - } else { - /* istanbul ignore if */ - { - if (type === 'component' && config.isReservedTag(id)) { - warn( - 'Do not use built-in or reserved HTML elements as component ' + - 'id: ' + id - ); - } - } - if (type === 'component' && isPlainObject(definition)) { - definition.name = definition.name || id; - definition = Vue.extend(definition); - } - if (type === 'directive' && typeof definition === 'function') { - definition = { bind: definition, update: definition }; - } - this.options[type + 's'][id] = definition; - return definition - } - }; - }); -} - -var KeepAlive = { - name: 'keep-alive', - abstract: true, - created: function created () { - this.cache = Object.create(null); - }, - render: function render () { - var vnode = getFirstComponentChild(this.$slots.default); - if (vnode && vnode.componentOptions) { - var opts = vnode.componentOptions; - var key = vnode.key == null - // same constructor may get registered as different local components - // so cid alone is not enough (#3269) - ? opts.Ctor.cid + '::' + opts.tag - : vnode.key; - if (this.cache[key]) { - vnode.child = this.cache[key].child; - } else { - this.cache[key] = vnode; - } - vnode.data.keepAlive = true; - } - return vnode - }, - destroyed: function destroyed () { - var this$1 = this; - - for (var key in this.cache) { - var vnode = this$1.cache[key]; - callHook(vnode.child, 'deactivated'); - vnode.child.$destroy(); - } - } -}; - -var builtInComponents = { - KeepAlive: KeepAlive -}; - -/* */ - -function initGlobalAPI (Vue) { - // config - var configDef = {}; - configDef.get = function () { return config; }; - { - configDef.set = function () { - warn( - 'Do not replace the Vue.config object, set individual fields instead.' - ); - }; - } - Object.defineProperty(Vue, 'config', configDef); - Vue.util = util; - Vue.set = set; - Vue.delete = del; - Vue.nextTick = nextTick; - - Vue.options = Object.create(null); - config._assetTypes.forEach(function (type) { - Vue.options[type + 's'] = Object.create(null); - }); - - extend(Vue.options.components, builtInComponents); - - initUse(Vue); - initMixin$1(Vue); - initExtend(Vue); - initAssetRegisters(Vue); -} - -initGlobalAPI(Vue$3); - -Object.defineProperty(Vue$3.prototype, '$isServer', { - get: function () { return config._isServer; } -}); - -Vue$3.version = '2.0.3'; - -/* */ - -// attributes that should be using props for binding -var mustUseProp = makeMap('value,selected,checked,muted'); - -var isEnumeratedAttr = makeMap('contenteditable,draggable,spellcheck'); - -var isBooleanAttr = makeMap( - 'allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,' + - 'default,defaultchecked,defaultmuted,defaultselected,defer,disabled,' + - 'enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,' + - 'muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,' + - 'required,reversed,scoped,seamless,selected,sortable,translate,' + - 'truespeed,typemustmatch,visible' -); - -var isAttr = makeMap( - 'accept,accept-charset,accesskey,action,align,alt,async,autocomplete,' + - 'autofocus,autoplay,autosave,bgcolor,border,buffered,challenge,charset,' + - 'checked,cite,class,code,codebase,color,cols,colspan,content,http-equiv,' + - 'name,contenteditable,contextmenu,controls,coords,data,datetime,default,' + - 'defer,dir,dirname,disabled,download,draggable,dropzone,enctype,method,for,' + - 'form,formaction,headers,
,height,hidden,high,href,hreflang,http-equiv,' + - 'icon,id,ismap,itemprop,keytype,kind,label,lang,language,list,loop,low,' + - 'manifest,max,maxlength,media,method,GET,POST,min,multiple,email,file,' + - 'muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,' + - 'preload,radiogroup,readonly,rel,required,reversed,rows,rowspan,sandbox,' + - 'scope,scoped,seamless,selected,shape,size,type,text,password,sizes,span,' + - 'spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,' + - 'target,title,type,usemap,value,width,wrap' -); - - - -var xlinkNS = 'http://www.w3.org/1999/xlink'; - -var isXlink = function (name) { - return name.charAt(5) === ':' && name.slice(0, 5) === 'xlink' -}; - -var getXlinkProp = function (name) { - return isXlink(name) ? name.slice(6, name.length) : '' -}; - -var isFalsyAttrValue = function (val) { - return val == null || val === false -}; - -/* */ - -function genClassForVnode (vnode) { - var data = vnode.data; - var parentNode = vnode; - var childNode = vnode; - while (childNode.child) { - childNode = childNode.child._vnode; - if (childNode.data) { - data = mergeClassData(childNode.data, data); - } - } - while ((parentNode = parentNode.parent)) { - if (parentNode.data) { - data = mergeClassData(data, parentNode.data); - } - } - return genClassFromData(data) -} - -function mergeClassData (child, parent) { - return { - staticClass: concat(child.staticClass, parent.staticClass), - class: child.class - ? [child.class, parent.class] - : parent.class - } -} - -function genClassFromData (data) { - var dynamicClass = data.class; - var staticClass = data.staticClass; - if (staticClass || dynamicClass) { - return concat(staticClass, stringifyClass(dynamicClass)) - } - /* istanbul ignore next */ - return '' -} - -function concat (a, b) { - return a ? b ? (a + ' ' + b) : a : (b || '') -} - -function stringifyClass (value) { - var res = ''; - if (!value) { - return res - } - if (typeof value === 'string') { - return value - } - if (Array.isArray(value)) { - var stringified; - for (var i = 0, l = value.length; i < l; i++) { - if (value[i]) { - if ((stringified = stringifyClass(value[i]))) { - res += stringified + ' '; - } - } - } - return res.slice(0, -1) - } - if (isObject(value)) { - for (var key in value) { - if (value[key]) { res += key + ' '; } - } - return res.slice(0, -1) - } - /* istanbul ignore next */ - return res -} - -/* */ - -var namespaceMap = { - svg: 'http://www.w3.org/2000/svg', - math: 'http://www.w3.org/1998/Math/MathML' -}; - -var isHTMLTag = makeMap( - 'html,body,base,head,link,meta,style,title,' + - 'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' + - 'div,dd,dl,dt,figcaption,figure,hr,img,li,main,ol,p,pre,ul,' + - 'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' + - 's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' + - 'embed,object,param,source,canvas,script,noscript,del,ins,' + - 'caption,col,colgroup,table,thead,tbody,td,th,tr,' + - 'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' + - 'output,progress,select,textarea,' + - 'details,dialog,menu,menuitem,summary,' + - 'content,element,shadow,template' -); - -var isUnaryTag = makeMap( - 'area,base,br,col,embed,frame,hr,img,input,isindex,keygen,' + - 'link,meta,param,source,track,wbr', - true -); - -// Elements that you can, intentionally, leave open -// (and which close themselves) -var canBeLeftOpenTag = makeMap( - 'colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source', - true -); - -// HTML5 tags https://html.spec.whatwg.org/multipage/indices.html#elements-3 -// Phrasing Content https://html.spec.whatwg.org/multipage/dom.html#phrasing-content -var isNonPhrasingTag = makeMap( - 'address,article,aside,base,blockquote,body,caption,col,colgroup,dd,' + - 'details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,' + - 'h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,' + - 'optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,' + - 'title,tr,track', - true -); - -// this map is intentionally selective, only covering SVG elements that may -// contain child elements. -var isSVG = makeMap( - 'svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font,' + - 'font-face,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,' + - 'polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view', - true -); - -var isPreTag = function (tag) { return tag === 'pre'; }; - -var isReservedTag = function (tag) { - return isHTMLTag(tag) || isSVG(tag) -}; - -function getTagNamespace (tag) { - if (isSVG(tag)) { - return 'svg' - } - // basic support for MathML - // note it doesn't support other MathML elements being component roots - if (tag === 'math') { - return 'math' - } -} - -var unknownElementCache = Object.create(null); -function isUnknownElement (tag) { - /* istanbul ignore if */ - if (!inBrowser) { - return true - } - if (isReservedTag(tag)) { - return false - } - tag = tag.toLowerCase(); - /* istanbul ignore if */ - if (unknownElementCache[tag] != null) { - return unknownElementCache[tag] - } - var el = document.createElement(tag); - if (tag.indexOf('-') > -1) { - // http://stackoverflow.com/a/28210364/1070244 - return (unknownElementCache[tag] = ( - el.constructor === window.HTMLUnknownElement || - el.constructor === window.HTMLElement - )) - } else { - return (unknownElementCache[tag] = /HTMLUnknownElement/.test(el.toString())) - } -} - -/* */ - -/** - * Query an element selector if it's not an element already. - */ -function query (el) { - if (typeof el === 'string') { - var selector = el; - el = document.querySelector(el); - if (!el) { - "development" !== 'production' && warn( - 'Cannot find element: ' + selector - ); - return document.createElement('div') - } - } - return el -} - -/* */ - -function createElement$1 (tagName, vnode) { - var elm = document.createElement(tagName); - if (tagName !== 'select') { - return elm - } - if (vnode.data && vnode.data.attrs && 'multiple' in vnode.data.attrs) { - elm.setAttribute('multiple', 'multiple'); - } - return elm -} - -function createElementNS (namespace, tagName) { - return document.createElementNS(namespaceMap[namespace], tagName) -} - -function createTextNode (text) { - return document.createTextNode(text) -} - -function createComment (text) { - return document.createComment(text) -} - -function insertBefore (parentNode, newNode, referenceNode) { - parentNode.insertBefore(newNode, referenceNode); -} - -function removeChild (node, child) { - node.removeChild(child); -} - -function appendChild (node, child) { - node.appendChild(child); -} - -function parentNode (node) { - return node.parentNode -} - -function nextSibling (node) { - return node.nextSibling -} - -function tagName (node) { - return node.tagName -} - -function setTextContent (node, text) { - node.textContent = text; -} - -function childNodes (node) { - return node.childNodes -} - -function setAttribute (node, key, val) { - node.setAttribute(key, val); -} - - -var nodeOps = Object.freeze({ - createElement: createElement$1, - createElementNS: createElementNS, - createTextNode: createTextNode, - createComment: createComment, - insertBefore: insertBefore, - removeChild: removeChild, - appendChild: appendChild, - parentNode: parentNode, - nextSibling: nextSibling, - tagName: tagName, - setTextContent: setTextContent, - childNodes: childNodes, - setAttribute: setAttribute -}); - -/* */ - -var ref = { - create: function create (_, vnode) { - registerRef(vnode); - }, - update: function update (oldVnode, vnode) { - if (oldVnode.data.ref !== vnode.data.ref) { - registerRef(oldVnode, true); - registerRef(vnode); - } - }, - destroy: function destroy (vnode) { - registerRef(vnode, true); - } -}; - -function registerRef (vnode, isRemoval) { - var key = vnode.data.ref; - if (!key) { return } - - var vm = vnode.context; - var ref = vnode.child || vnode.elm; - var refs = vm.$refs; - if (isRemoval) { - if (Array.isArray(refs[key])) { - remove$1(refs[key], ref); - } else if (refs[key] === ref) { - refs[key] = undefined; - } - } else { - if (vnode.data.refInFor) { - if (Array.isArray(refs[key])) { - refs[key].push(ref); - } else { - refs[key] = [ref]; - } - } else { - refs[key] = ref; - } - } -} - -/** - * Virtual DOM patching algorithm based on Snabbdom by - * Simon Friis Vindum (@paldepind) - * Licensed under the MIT License - * https://github.com/paldepind/snabbdom/blob/master/LICENSE - * - * modified by Evan You (@yyx990803) - * - -/* - * Not type-checking this because this file is perf-critical and the cost - * of making flow understand it is not worth it. - */ - -var emptyNode = new VNode('', {}, []); - -var hooks$1 = ['create', 'update', 'remove', 'destroy']; - -function isUndef (s) { - return s == null -} - -function isDef (s) { - return s != null -} - -function sameVnode (vnode1, vnode2) { - return ( - vnode1.key === vnode2.key && - vnode1.tag === vnode2.tag && - vnode1.isComment === vnode2.isComment && - !vnode1.data === !vnode2.data - ) -} - -function createKeyToOldIdx (children, beginIdx, endIdx) { - var i, key; - var map = {}; - for (i = beginIdx; i <= endIdx; ++i) { - key = children[i].key; - if (isDef(key)) { map[key] = i; } - } - return map -} - -function createPatchFunction (backend) { - var i, j; - var cbs = {}; - - var modules = backend.modules; - var nodeOps = backend.nodeOps; - - for (i = 0; i < hooks$1.length; ++i) { - cbs[hooks$1[i]] = []; - for (j = 0; j < modules.length; ++j) { - if (modules[j][hooks$1[i]] !== undefined) { cbs[hooks$1[i]].push(modules[j][hooks$1[i]]); } - } - } - - function emptyNodeAt (elm) { - return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm) - } - - function createRmCb (childElm, listeners) { - function remove$$1 () { - if (--remove$$1.listeners === 0) { - removeElement(childElm); - } - } - remove$$1.listeners = listeners; - return remove$$1 - } - - function removeElement (el) { - var parent = nodeOps.parentNode(el); - nodeOps.removeChild(parent, el); - } - - function createElm (vnode, insertedVnodeQueue, nested) { - var i; - var data = vnode.data; - vnode.isRootInsert = !nested; - if (isDef(data)) { - if (isDef(i = data.hook) && isDef(i = i.init)) { i(vnode); } - // after calling the init hook, if the vnode is a child component - // it should've created a child instance and mounted it. the child - // component also has set the placeholder vnode's elm. - // in that case we can just return the element and be done. - if (isDef(i = vnode.child)) { - initComponent(vnode, insertedVnodeQueue); - return vnode.elm - } - } - var children = vnode.children; - var tag = vnode.tag; - if (isDef(tag)) { - { - if ( - !vnode.ns && - !(config.ignoredElements && config.ignoredElements.indexOf(tag) > -1) && - config.isUnknownElement(tag) - ) { - warn( - 'Unknown custom element: <' + tag + '> - did you ' + - 'register the component correctly? For recursive components, ' + - 'make sure to provide the "name" option.', - vnode.context - ); - } - } - vnode.elm = vnode.ns - ? nodeOps.createElementNS(vnode.ns, tag) - : nodeOps.createElement(tag, vnode); - setScope(vnode); - createChildren(vnode, children, insertedVnodeQueue); - if (isDef(data)) { - invokeCreateHooks(vnode, insertedVnodeQueue); - } - } else if (vnode.isComment) { - vnode.elm = nodeOps.createComment(vnode.text); - } else { - vnode.elm = nodeOps.createTextNode(vnode.text); - } - return vnode.elm - } - - function createChildren (vnode, children, insertedVnodeQueue) { - if (Array.isArray(children)) { - for (var i = 0; i < children.length; ++i) { - nodeOps.appendChild(vnode.elm, createElm(children[i], insertedVnodeQueue, true)); - } - } else if (isPrimitive(vnode.text)) { - nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(vnode.text)); - } - } - - function isPatchable (vnode) { - while (vnode.child) { - vnode = vnode.child._vnode; - } - return isDef(vnode.tag) - } - - function invokeCreateHooks (vnode, insertedVnodeQueue) { - for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) { - cbs.create[i$1](emptyNode, vnode); - } - i = vnode.data.hook; // Reuse variable - if (isDef(i)) { - if (i.create) { i.create(emptyNode, vnode); } - if (i.insert) { insertedVnodeQueue.push(vnode); } - } - } - - function initComponent (vnode, insertedVnodeQueue) { - if (vnode.data.pendingInsert) { - insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert); - } - vnode.elm = vnode.child.$el; - if (isPatchable(vnode)) { - invokeCreateHooks(vnode, insertedVnodeQueue); - setScope(vnode); - } else { - // empty component root. - // skip all element-related modules except for ref (#3455) - registerRef(vnode); - // make sure to invoke the insert hook - insertedVnodeQueue.push(vnode); - } - } - - // set scope id attribute for scoped CSS. - // this is implemented as a special case to avoid the overhead - // of going through the normal attribute patching process. - function setScope (vnode) { - var i; - if (isDef(i = vnode.context) && isDef(i = i.$options._scopeId)) { - nodeOps.setAttribute(vnode.elm, i, ''); - } - if (isDef(i = activeInstance) && - i !== vnode.context && - isDef(i = i.$options._scopeId)) { - nodeOps.setAttribute(vnode.elm, i, ''); - } - } - - function addVnodes (parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) { - for (; startIdx <= endIdx; ++startIdx) { - nodeOps.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before); - } - } - - function invokeDestroyHook (vnode) { - var i, j; - var data = vnode.data; - if (isDef(data)) { - if (isDef(i = data.hook) && isDef(i = i.destroy)) { i(vnode); } - for (i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](vnode); } - } - if (isDef(i = vnode.children)) { - for (j = 0; j < vnode.children.length; ++j) { - invokeDestroyHook(vnode.children[j]); - } - } - } - - function removeVnodes (parentElm, vnodes, startIdx, endIdx) { - for (; startIdx <= endIdx; ++startIdx) { - var ch = vnodes[startIdx]; - if (isDef(ch)) { - if (isDef(ch.tag)) { - removeAndInvokeRemoveHook(ch); - invokeDestroyHook(ch); - } else { // Text node - nodeOps.removeChild(parentElm, ch.elm); - } - } - } - } - - function removeAndInvokeRemoveHook (vnode, rm) { - if (rm || isDef(vnode.data)) { - var listeners = cbs.remove.length + 1; - if (!rm) { - // directly removing - rm = createRmCb(vnode.elm, listeners); - } else { - // we have a recursively passed down rm callback - // increase the listeners count - rm.listeners += listeners; - } - // recursively invoke hooks on child component root node - if (isDef(i = vnode.child) && isDef(i = i._vnode) && isDef(i.data)) { - removeAndInvokeRemoveHook(i, rm); - } - for (i = 0; i < cbs.remove.length; ++i) { - cbs.remove[i](vnode, rm); - } - if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) { - i(vnode, rm); - } else { - rm(); - } - } else { - removeElement(vnode.elm); - } - } - - function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { - var oldStartIdx = 0; - var newStartIdx = 0; - var oldEndIdx = oldCh.length - 1; - var oldStartVnode = oldCh[0]; - var oldEndVnode = oldCh[oldEndIdx]; - var newEndIdx = newCh.length - 1; - var newStartVnode = newCh[0]; - var newEndVnode = newCh[newEndIdx]; - var oldKeyToIdx, idxInOld, elmToMove, before; - - // removeOnly is a special flag used only by - // to ensure removed elements stay in correct relative positions - // during leaving transitions - var canMove = !removeOnly; - - while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { - if (isUndef(oldStartVnode)) { - oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left - } else if (isUndef(oldEndVnode)) { - oldEndVnode = oldCh[--oldEndIdx]; - } else if (sameVnode(oldStartVnode, newStartVnode)) { - patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); - oldStartVnode = oldCh[++oldStartIdx]; - newStartVnode = newCh[++newStartIdx]; - } else if (sameVnode(oldEndVnode, newEndVnode)) { - patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); - oldEndVnode = oldCh[--oldEndIdx]; - newEndVnode = newCh[--newEndIdx]; - } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right - patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); - canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)); - oldStartVnode = oldCh[++oldStartIdx]; - newEndVnode = newCh[--newEndIdx]; - } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left - patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); - canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); - oldEndVnode = oldCh[--oldEndIdx]; - newStartVnode = newCh[++newStartIdx]; - } else { - if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); } - idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null; - if (isUndef(idxInOld)) { // New element - nodeOps.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); - newStartVnode = newCh[++newStartIdx]; - } else { - elmToMove = oldCh[idxInOld]; - /* istanbul ignore if */ - if ("development" !== 'production' && !elmToMove) { - warn( - 'It seems there are duplicate keys that is causing an update error. ' + - 'Make sure each v-for item has a unique key.' - ); - } - if (elmToMove.tag !== newStartVnode.tag) { - // same key but different element. treat as new element - nodeOps.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); - newStartVnode = newCh[++newStartIdx]; - } else { - patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); - oldCh[idxInOld] = undefined; - canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm); - newStartVnode = newCh[++newStartIdx]; - } - } - } - } - if (oldStartIdx > oldEndIdx) { - before = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm; - addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); - } else if (newStartIdx > newEndIdx) { - removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); - } - } - - function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { - if (oldVnode === vnode) { - return - } - // reuse element for static trees. - // note we only do this if the vnode is cloned - - // if the new node is not cloned it means the render functions have been - // reset by the hot-reload-api and we need to do a proper re-render. - if (vnode.isStatic && - oldVnode.isStatic && - vnode.key === oldVnode.key && - vnode.isCloned) { - vnode.elm = oldVnode.elm; - return - } - var i; - var data = vnode.data; - var hasData = isDef(data); - if (hasData && isDef(i = data.hook) && isDef(i = i.prepatch)) { - i(oldVnode, vnode); - } - var elm = vnode.elm = oldVnode.elm; - var oldCh = oldVnode.children; - var ch = vnode.children; - if (hasData && isPatchable(vnode)) { - for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); } - if (isDef(i = data.hook) && isDef(i = i.update)) { i(oldVnode, vnode); } - } - if (isUndef(vnode.text)) { - if (isDef(oldCh) && isDef(ch)) { - if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); } - } else if (isDef(ch)) { - if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); } - addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); - } else if (isDef(oldCh)) { - removeVnodes(elm, oldCh, 0, oldCh.length - 1); - } else if (isDef(oldVnode.text)) { - nodeOps.setTextContent(elm, ''); - } - } else if (oldVnode.text !== vnode.text) { - nodeOps.setTextContent(elm, vnode.text); - } - if (hasData) { - if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); } - } - } - - function invokeInsertHook (vnode, queue, initial) { - // delay insert hooks for component root nodes, invoke them after the - // element is really inserted - if (initial && vnode.parent) { - vnode.parent.data.pendingInsert = queue; - } else { - for (var i = 0; i < queue.length; ++i) { - queue[i].data.hook.insert(queue[i]); - } - } - } - - var bailed = false; - function hydrate (elm, vnode, insertedVnodeQueue) { - { - if (!assertNodeMatch(elm, vnode)) { - return false - } - } - vnode.elm = elm; - var tag = vnode.tag; - var data = vnode.data; - var children = vnode.children; - if (isDef(data)) { - if (isDef(i = data.hook) && isDef(i = i.init)) { i(vnode, true /* hydrating */); } - if (isDef(i = vnode.child)) { - // child component. it should have hydrated its own tree. - initComponent(vnode, insertedVnodeQueue); - return true - } - } - if (isDef(tag)) { - if (isDef(children)) { - var childNodes = nodeOps.childNodes(elm); - // empty element, allow client to pick up and populate children - if (!childNodes.length) { - createChildren(vnode, children, insertedVnodeQueue); - } else { - var childrenMatch = true; - if (childNodes.length !== children.length) { - childrenMatch = false; - } else { - for (var i$1 = 0; i$1 < children.length; i$1++) { - if (!hydrate(childNodes[i$1], children[i$1], insertedVnodeQueue)) { - childrenMatch = false; - break - } - } - } - if (!childrenMatch) { - if ("development" !== 'production' && - typeof console !== 'undefined' && - !bailed) { - bailed = true; - console.warn('Parent: ', elm); - console.warn('Mismatching childNodes vs. VNodes: ', childNodes, children); - } - return false - } - } - } - if (isDef(data)) { - invokeCreateHooks(vnode, insertedVnodeQueue); - } - } - return true - } - - function assertNodeMatch (node, vnode) { - if (vnode.tag) { - return ( - vnode.tag.indexOf('vue-component') === 0 || - vnode.tag === nodeOps.tagName(node).toLowerCase() - ) - } else { - return _toString(vnode.text) === node.data - } - } - - return function patch (oldVnode, vnode, hydrating, removeOnly) { - if (!vnode) { - if (oldVnode) { invokeDestroyHook(oldVnode); } - return - } - - var elm, parent; - var isInitialPatch = false; - var insertedVnodeQueue = []; - - if (!oldVnode) { - // empty mount, create new root element - isInitialPatch = true; - createElm(vnode, insertedVnodeQueue); - } else { - var isRealElement = isDef(oldVnode.nodeType); - if (!isRealElement && sameVnode(oldVnode, vnode)) { - patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly); - } else { - if (isRealElement) { - // mounting to a real element - // check if this is server-rendered content and if we can perform - // a successful hydration. - if (oldVnode.nodeType === 1 && oldVnode.hasAttribute('server-rendered')) { - oldVnode.removeAttribute('server-rendered'); - hydrating = true; - } - if (hydrating) { - if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { - invokeInsertHook(vnode, insertedVnodeQueue, true); - return oldVnode - } else { - warn( - 'The client-side rendered virtual DOM tree is not matching ' + - 'server-rendered content. This is likely caused by incorrect ' + - 'HTML markup, for example nesting block-level elements inside ' + - '

, or missing